fontastic 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/claude-code-review.yml +0 -6
- package/.github/workflows/release-please.yml +25 -0
- package/.github/workflows/release.yml +5 -109
- package/.release-please-manifest.json +3 -0
- package/CHANGELOG.md +39 -0
- package/README.md +6 -0
- package/app/Application.js +4 -4
- package/app/Application.ts +13 -13
- package/app/config/database.js +1 -1
- package/app/config/database.ts +1 -1
- package/app/config/mimes.js +23 -23
- package/app/config/mimes.ts +35 -29
- package/app/core/ConfigManager.js +27 -0
- package/app/core/ConfigManager.ts +28 -0
- package/app/core/FontFinder.js +66 -15
- package/app/core/FontFinder.ts +81 -22
- package/app/core/FontManager.js +20 -18
- package/app/core/FontManager.ts +21 -19
- package/app/core/FontObject.js +44 -24
- package/app/core/FontObject.ts +47 -27
- package/app/core/MessageHandler.js +70 -19
- package/app/core/MessageHandler.ts +82 -21
- package/app/core/SystemManager.js +5 -1
- package/app/core/SystemManager.ts +5 -1
- package/app/database/entity/Collection.schema.js +20 -18
- package/app/database/entity/Collection.schema.ts +22 -21
- package/app/database/repository/Collection.repository.js +17 -18
- package/app/database/repository/Collection.repository.ts +27 -18
- package/app/database/repository/Store.repository.js +13 -18
- package/app/database/repository/Store.repository.ts +13 -21
- package/app/enums/ChannelType.js +18 -0
- package/app/enums/ChannelType.ts +24 -0
- package/app/main.js +98 -10
- package/app/main.ts +126 -19
- package/app/package.json +1 -1
- package/app/types/NativeThemeState.js +3 -0
- package/app/types/NativeThemeState.ts +4 -0
- package/app/types/ScanProgress.js +3 -0
- package/app/types/ScanProgress.ts +6 -0
- package/app/types/SystemPreferencesState.js +3 -0
- package/app/types/SystemPreferencesState.ts +4 -0
- package/app/types/index.js +3 -0
- package/app/types/index.ts +3 -0
- package/package.json +2 -2
- package/release-please-config.json +20 -0
- package/scripts/patch-electron-plist.js +41 -0
- package/src/app/core/services/database/database.service.ts +6 -0
- package/src/app/core/services/message/message.service.ts +33 -1
- package/src/app/core/services/presentation/presentation.service.ts +100 -1
- package/src/app/layout/footer/footer.component.html +13 -2
- package/src/app/layout/footer/footer.component.ts +18 -2
- package/src/app/layout/header/header.component.html +0 -10
- package/src/app/layout/header/header.component.ts +4 -23
- package/src/app/layout/navigation/navigation.component.html +66 -16
- package/src/app/layout/navigation/navigation.component.ts +65 -12
- package/src/app/settings/ai-keys/ai-keys.component.ts +13 -18
- package/src/app/settings/danger-zone/danger-zone.component.html +8 -0
- package/src/app/settings/danger-zone/danger-zone.component.ts +12 -0
- package/src/app/settings/news-api/news-api.component.ts +6 -8
- package/src/app/settings/theme/theme.component.html +15 -2
- package/src/app/settings/theme/theme.component.ts +4 -0
- package/src/app/shared/components/datagrid/datagrid.component.html +8 -17
- package/src/app/shared/components/datagrid/datagrid.component.ts +6 -10
- package/src/app/shared/components/glyphs/glyphs.component.html +5 -15
- package/src/app/shared/components/glyphs/glyphs.component.ts +3 -0
- package/src/app/shared/components/preview/preview.component.html +1 -1
- package/src/app/shared/components/preview/preview.component.ts +3 -8
- package/src/app/shared/components/prompt-dialog/prompt-dialog.component.html +2 -2
- package/src/app/shared/components/prompt-dialog/prompt-dialog.component.ts +2 -1
- package/src/app/shared/components/rule-builder/rule-builder.component.html +18 -6
- package/src/app/shared/components/rule-builder/rule-builder.component.ts +34 -2
- package/src/app/shared/components/search/search.component.html +9 -36
- package/src/app/shared/components/search/search.component.ts +2 -1
- package/src/app/shared/components/waterfall/waterfall.component.html +1 -3
- package/src/app/shared/components/waterfall/waterfall.component.ts +2 -1
- package/src/app/shared/directives/disabled-opacity/disabled-opacity.directive.ts +18 -0
- package/src/app/shared/directives/hover-highlight/hover-highlight.directive.ts +38 -0
- package/src/app/shared/directives/index.ts +5 -0
- package/src/app/shared/directives/modal-backdrop/modal-backdrop.directive.ts +18 -0
- package/src/app/shared/directives/scroll-reset/scroll-reset.directive.ts +15 -0
- package/src/app/shared/directives/stop-propagation/stop-propagation.directive.ts +12 -0
- package/src/assets/icons/favicon.256x256.png +0 -0
- package/src/assets/icons/favicon.512x512.png +0 -0
- package/src/assets/icons/favicon.icns +0 -0
- package/src/assets/icons/favicon.ico +0 -0
- package/src/assets/icons/favicon.png +0 -0
- package/src/favicon.ico +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
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 {
|
|
7
|
+
import { RuleBuilderComponent } from '../../shared/components';
|
|
8
8
|
import { AutofocusDirective } from '../../shared/directives/autofocus/autofocus.directive';
|
|
9
|
+
import { HoverHighlightDirective } from '../../shared/directives/hover-highlight/hover-highlight.directive';
|
|
10
|
+
import { StopPropagationDirective } from '../../shared/directives/stop-propagation/stop-propagation.directive';
|
|
9
11
|
import { LibraryComponent } from './library/library.component';
|
|
10
12
|
import { NewsStatsComponent } from './stats/stats.component';
|
|
11
13
|
import type { Collection } from '@main/database/entity/Collection.schema';
|
|
@@ -26,9 +28,10 @@ export interface TreeNode {
|
|
|
26
28
|
FormsModule,
|
|
27
29
|
CollapsiblePanelComponent,
|
|
28
30
|
ContextMenuComponent,
|
|
29
|
-
PromptDialogComponent,
|
|
30
31
|
RuleBuilderComponent,
|
|
31
32
|
AutofocusDirective,
|
|
33
|
+
HoverHighlightDirective,
|
|
34
|
+
StopPropagationDirective,
|
|
32
35
|
LibraryComponent,
|
|
33
36
|
NewsStatsComponent,
|
|
34
37
|
],
|
|
@@ -39,6 +42,15 @@ export class NavigationComponent implements OnInit, OnDestroy {
|
|
|
39
42
|
private message = inject(MessageService);
|
|
40
43
|
private presentation = inject(PresentationService);
|
|
41
44
|
|
|
45
|
+
constructor() {
|
|
46
|
+
effect(() => {
|
|
47
|
+
const req = this.presentation.createRootCollectionRequest();
|
|
48
|
+
if (req > 0) {
|
|
49
|
+
this.openCreateRootCollection();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
private menuToggleListener = (_event: any, panel: string) => {
|
|
43
55
|
if (panel === 'expand-collections') {
|
|
44
56
|
this.expandAll();
|
|
@@ -72,7 +84,8 @@ export class NavigationComponent implements OnInit, OnDestroy {
|
|
|
72
84
|
editingTitle = '';
|
|
73
85
|
allExpanded = false;
|
|
74
86
|
|
|
75
|
-
|
|
87
|
+
isCreating = false;
|
|
88
|
+
creatingTitle = '';
|
|
76
89
|
pendingParentId: number | null = null;
|
|
77
90
|
|
|
78
91
|
// Smart Collection state
|
|
@@ -256,7 +269,9 @@ export class NavigationComponent implements OnInit, OnDestroy {
|
|
|
256
269
|
switch (action) {
|
|
257
270
|
case 'add-collection':
|
|
258
271
|
this.pendingParentId = collection.id;
|
|
259
|
-
this.
|
|
272
|
+
this.isCreating = true;
|
|
273
|
+
this.creatingTitle = '';
|
|
274
|
+
this.presentation.expandNavigationId(collection.id);
|
|
260
275
|
break;
|
|
261
276
|
case 'add-fonts':
|
|
262
277
|
this.handleAddFonts(collection);
|
|
@@ -273,17 +288,24 @@ export class NavigationComponent implements OnInit, OnDestroy {
|
|
|
273
288
|
|
|
274
289
|
openCreateRootCollection() {
|
|
275
290
|
this.pendingParentId = 0;
|
|
276
|
-
this.
|
|
291
|
+
this.isCreating = true;
|
|
292
|
+
this.creatingTitle = '';
|
|
277
293
|
}
|
|
278
294
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
295
|
+
saveCreating() {
|
|
296
|
+
const title = this.creatingTitle.trim();
|
|
297
|
+
if (title) {
|
|
298
|
+
this.db.collectionCreate({ title, parentId: this.pendingParentId });
|
|
299
|
+
if (this.pendingParentId) {
|
|
300
|
+
this.presentation.expandNavigationId(this.pendingParentId);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
this.cancelCreating();
|
|
283
304
|
}
|
|
284
305
|
|
|
285
|
-
|
|
286
|
-
this.
|
|
306
|
+
cancelCreating() {
|
|
307
|
+
this.isCreating = false;
|
|
308
|
+
this.creatingTitle = '';
|
|
287
309
|
this.pendingParentId = null;
|
|
288
310
|
}
|
|
289
311
|
|
|
@@ -458,6 +480,37 @@ export class NavigationComponent implements OnInit, OnDestroy {
|
|
|
458
480
|
this.editingSmartCollection = null;
|
|
459
481
|
}
|
|
460
482
|
|
|
483
|
+
onRuleBuilderSavedAndSync(data: { title: string; rules: SmartCollectionRule[]; match_type: string }) {
|
|
484
|
+
const rulesJson = JSON.stringify(data.rules);
|
|
485
|
+
if (this.editingSmartCollection) {
|
|
486
|
+
const id = this.editingSmartCollection.id;
|
|
487
|
+
this.db
|
|
488
|
+
.smartCollectionUpdate(id, {
|
|
489
|
+
title: data.title,
|
|
490
|
+
rules: rulesJson,
|
|
491
|
+
match_type: data.match_type,
|
|
492
|
+
})
|
|
493
|
+
.then(() => {
|
|
494
|
+
this.db.selectSmartCollection(id);
|
|
495
|
+
});
|
|
496
|
+
} else {
|
|
497
|
+
this.db
|
|
498
|
+
.smartCollectionCreate({
|
|
499
|
+
title: data.title,
|
|
500
|
+
rules: rulesJson,
|
|
501
|
+
match_type: data.match_type,
|
|
502
|
+
})
|
|
503
|
+
.then((collections) => {
|
|
504
|
+
const created = collections.find((c) => c.title === data.title);
|
|
505
|
+
if (created) {
|
|
506
|
+
this.db.selectSmartCollection(created.id);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
this.showRuleBuilder = false;
|
|
511
|
+
this.editingSmartCollection = null;
|
|
512
|
+
}
|
|
513
|
+
|
|
461
514
|
onRuleBuilderCancelled() {
|
|
462
515
|
this.showRuleBuilder = false;
|
|
463
516
|
this.editingSmartCollection = null;
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { Component, inject, signal, OnInit } from '@angular/core';
|
|
2
2
|
import { FormsModule } from '@angular/forms';
|
|
3
3
|
import { MessageService } from '../../core/services';
|
|
4
|
-
import { StorageType } from '@main/enums';
|
|
5
|
-
|
|
6
|
-
interface AiKeys {
|
|
7
|
-
anthropic: string;
|
|
8
|
-
google: string;
|
|
9
|
-
openai: string;
|
|
10
|
-
}
|
|
11
4
|
|
|
12
5
|
@Component({
|
|
13
6
|
selector: 'app-settings-ai-keys',
|
|
@@ -24,20 +17,22 @@ export class SettingsAiKeysComponent implements OnInit {
|
|
|
24
17
|
readonly saveStatus = signal<'idle' | 'saved'>('idle');
|
|
25
18
|
|
|
26
19
|
async ngOnInit() {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
const [anthropic, google, openai] = await Promise.all([
|
|
21
|
+
this.message.safeRetrieve('secure.ai.anthropic'),
|
|
22
|
+
this.message.safeRetrieve('secure.ai.google'),
|
|
23
|
+
this.message.safeRetrieve('secure.ai.openai'),
|
|
24
|
+
]);
|
|
25
|
+
if (anthropic) this.anthropicKey.set(anthropic);
|
|
26
|
+
if (google) this.googleKey.set(google);
|
|
27
|
+
if (openai) this.openaiKey.set(openai);
|
|
33
28
|
}
|
|
34
29
|
|
|
35
30
|
async onSave() {
|
|
36
|
-
await
|
|
37
|
-
anthropic
|
|
38
|
-
google
|
|
39
|
-
openai
|
|
40
|
-
|
|
31
|
+
await Promise.all([
|
|
32
|
+
this.message.safeStore('secure.ai.anthropic', this.anthropicKey()),
|
|
33
|
+
this.message.safeStore('secure.ai.google', this.googleKey()),
|
|
34
|
+
this.message.safeStore('secure.ai.openai', this.openaiKey()),
|
|
35
|
+
]);
|
|
41
36
|
this.saveStatus.set('saved');
|
|
42
37
|
setTimeout(() => this.saveStatus.set('idle'), 2000);
|
|
43
38
|
}
|
|
@@ -18,5 +18,13 @@
|
|
|
18
18
|
</div>
|
|
19
19
|
<button class="btn btn-sm btn-danger" (click)="onClearStore()">Clear</button>
|
|
20
20
|
</div>
|
|
21
|
+
|
|
22
|
+
<div class="flex items-center justify-between">
|
|
23
|
+
<div>
|
|
24
|
+
<p class="text-xs font-semibold" [style.color]="'var(--text-primary)'">Clear application cache</p>
|
|
25
|
+
<p class="text-xs" [style.color]="'var(--text-muted)'">Clears the Chromium session cache.</p>
|
|
26
|
+
</div>
|
|
27
|
+
<button class="btn btn-sm btn-danger" (click)="onClearCache()">Clear Cache</button>
|
|
28
|
+
</div>
|
|
21
29
|
</div>
|
|
22
30
|
</div>
|
|
@@ -34,4 +34,16 @@ export class SettingsDangerZoneComponent {
|
|
|
34
34
|
this.message.reloadWindow();
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
async onClearCache() {
|
|
39
|
+
const response = await this.message.showMessageBox({
|
|
40
|
+
type: 'question',
|
|
41
|
+
buttons: ['Yes', 'No'],
|
|
42
|
+
title: 'Confirm',
|
|
43
|
+
message: 'Are you sure you want to clear the application cache?',
|
|
44
|
+
});
|
|
45
|
+
if (response?.response === 0) {
|
|
46
|
+
await this.message.clearCache();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Component, inject, signal, OnInit } from '@angular/core';
|
|
2
2
|
import { FormsModule } from '@angular/forms';
|
|
3
3
|
import { MessageService, NewsService } from '../../core/services';
|
|
4
|
-
import { StorageType } from '@main/enums';
|
|
5
|
-
import type { NewsType } from '@main/types';
|
|
6
4
|
|
|
7
5
|
@Component({
|
|
8
6
|
selector: 'app-settings-news-api',
|
|
@@ -20,16 +18,16 @@ export class SettingsNewsApiComponent implements OnInit {
|
|
|
20
18
|
readonly fetchMessage = signal('');
|
|
21
19
|
|
|
22
20
|
async ngOnInit() {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
this.newsApiKey.set(
|
|
21
|
+
const apiKey = await this.message.safeRetrieve('secure.news.apiKey');
|
|
22
|
+
if (apiKey) {
|
|
23
|
+
this.newsApiKey.set(apiKey);
|
|
26
24
|
}
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
async onSaveNewsApiKey() {
|
|
30
28
|
const apiKey = this.newsApiKey();
|
|
31
29
|
if (!apiKey) return;
|
|
32
|
-
await this.message.
|
|
30
|
+
await this.message.safeStore('secure.news.apiKey', apiKey);
|
|
33
31
|
this.saveStatus.set('saved');
|
|
34
32
|
await this.news.refresh();
|
|
35
33
|
setTimeout(() => this.saveStatus.set('idle'), 2000);
|
|
@@ -39,8 +37,8 @@ export class SettingsNewsApiComponent implements OnInit {
|
|
|
39
37
|
this.fetchStatus.set('fetching');
|
|
40
38
|
this.fetchMessage.set('');
|
|
41
39
|
try {
|
|
42
|
-
|
|
43
|
-
const result = await this.message.fetchLatestNews({
|
|
40
|
+
// API key is retrieved securely on the main process side
|
|
41
|
+
const result = await this.message.fetchLatestNews({ country: 'us' });
|
|
44
42
|
const count = result?.articles?.length ?? 0;
|
|
45
43
|
this.fetchMessage.set(`Fetched ${count} article${count !== 1 ? 's' : ''}.`);
|
|
46
44
|
this.fetchStatus.set('done');
|
|
@@ -2,10 +2,23 @@
|
|
|
2
2
|
<div class="card-header">
|
|
3
3
|
<div class="card-title"><h3>Theme</h3></div>
|
|
4
4
|
</div>
|
|
5
|
-
<div class="card-body">
|
|
5
|
+
<div class="card-body space-y-4">
|
|
6
|
+
<div>
|
|
7
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
8
|
+
<input type="checkbox" class="accent-current" [ngModel]="presentation.autoTheme()" (ngModelChange)="onAutoThemeChange($event)" />
|
|
9
|
+
<span class="text-xs" [style.color]="'var(--text-secondary)'">Match system appearance</span>
|
|
10
|
+
</label>
|
|
11
|
+
</div>
|
|
6
12
|
<div>
|
|
7
13
|
<label class="form-label" for="theme">Selected theme</label>
|
|
8
|
-
<select
|
|
14
|
+
<select
|
|
15
|
+
id="theme"
|
|
16
|
+
class="form-select w-full"
|
|
17
|
+
[ngModel]="presentation.theme()"
|
|
18
|
+
(ngModelChange)="onThemeChange($event)"
|
|
19
|
+
[disabled]="presentation.autoTheme()"
|
|
20
|
+
name="theme"
|
|
21
|
+
>
|
|
9
22
|
@for (t of themes; track t.key) {
|
|
10
23
|
<option [value]="t.key">{{ t.label }}</option>
|
|
11
24
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="panel grid" [style.grid-template-rows]="db.totalPages() > 1 ? '1fr 32px' : '1fr'">
|
|
2
|
-
<div class="overflow-hidden scrollbox-y">
|
|
2
|
+
<div class="overflow-hidden scrollbox-y" [appScrollReset]="db.currentPage()">
|
|
3
3
|
<table class="w-full border-separate border-spacing-0">
|
|
4
4
|
<thead
|
|
5
5
|
class="sticky top-0 z-10"
|
|
@@ -44,14 +44,8 @@
|
|
|
44
44
|
class="cursor-pointer transition-colors"
|
|
45
45
|
[style.border-bottom]="'1px solid var(--border-subtle)'"
|
|
46
46
|
[id]="'grid-store-' + store.id"
|
|
47
|
-
[
|
|
47
|
+
[appHoverHighlight]="db.storeId() === store.id"
|
|
48
48
|
(click)="selectStore(store.id)"
|
|
49
|
-
(mouseenter)="
|
|
50
|
-
$any($event.currentTarget).style.backgroundColor = db.storeId() === store.id ? 'var(--selected-bg)' : 'var(--hover-bg)'
|
|
51
|
-
"
|
|
52
|
-
(mouseleave)="
|
|
53
|
-
$any($event.currentTarget).style.backgroundColor = db.storeId() === store.id ? 'var(--selected-bg)' : 'transparent'
|
|
54
|
-
"
|
|
55
49
|
>
|
|
56
50
|
<td class="w-10 px-2 py-1.5 text-center">
|
|
57
51
|
<span
|
|
@@ -60,7 +54,8 @@
|
|
|
60
54
|
[style.background]="
|
|
61
55
|
db.storeId() === store.id ? 'radial-gradient(circle, var(--accent) 40%, transparent 41%)' : 'transparent'
|
|
62
56
|
"
|
|
63
|
-
|
|
57
|
+
appStopPropagation
|
|
58
|
+
(click)="selectStore(store.id)"
|
|
64
59
|
></span>
|
|
65
60
|
</td>
|
|
66
61
|
<td class="px-4 py-1.5" [style.color]="'var(--text-primary)'">{{ store.full_name || store.file_name }}</td>
|
|
@@ -121,8 +116,7 @@
|
|
|
121
116
|
<button
|
|
122
117
|
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
123
118
|
[style.color]="'var(--text-secondary)'"
|
|
124
|
-
[
|
|
125
|
-
[class.opacity-30]="db.currentPage() === 1"
|
|
119
|
+
[appDisabledOpacity]="db.currentPage() === 1"
|
|
126
120
|
(click)="db.firstPage()"
|
|
127
121
|
>
|
|
128
122
|
<span
|
|
@@ -138,8 +132,7 @@
|
|
|
138
132
|
<button
|
|
139
133
|
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
140
134
|
[style.color]="'var(--text-secondary)'"
|
|
141
|
-
[
|
|
142
|
-
[class.opacity-30]="db.currentPage() === 1"
|
|
135
|
+
[appDisabledOpacity]="db.currentPage() === 1"
|
|
143
136
|
(click)="db.prevPage()"
|
|
144
137
|
>
|
|
145
138
|
<span
|
|
@@ -156,8 +149,7 @@
|
|
|
156
149
|
<button
|
|
157
150
|
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
158
151
|
[style.color]="'var(--text-secondary)'"
|
|
159
|
-
[
|
|
160
|
-
[class.opacity-30]="db.currentPage() === db.totalPages()"
|
|
152
|
+
[appDisabledOpacity]="db.currentPage() === db.totalPages()"
|
|
161
153
|
(click)="db.nextPage()"
|
|
162
154
|
>
|
|
163
155
|
<span
|
|
@@ -173,8 +165,7 @@
|
|
|
173
165
|
<button
|
|
174
166
|
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
175
167
|
[style.color]="'var(--text-secondary)'"
|
|
176
|
-
[
|
|
177
|
-
[class.opacity-30]="db.currentPage() === db.totalPages()"
|
|
168
|
+
[appDisabledOpacity]="db.currentPage() === db.totalPages()"
|
|
178
169
|
(click)="db.lastPage()"
|
|
179
170
|
>
|
|
180
171
|
<span
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { Component, inject
|
|
1
|
+
import { Component, inject } from '@angular/core';
|
|
2
2
|
import { DatabaseService, MessageService } from '../../../core/services';
|
|
3
|
+
import { DisabledOpacityDirective } from '../../directives/disabled-opacity/disabled-opacity.directive';
|
|
4
|
+
import { HoverHighlightDirective } from '../../directives/hover-highlight/hover-highlight.directive';
|
|
5
|
+
import { ScrollResetDirective } from '../../directives/scroll-reset/scroll-reset.directive';
|
|
6
|
+
import { StopPropagationDirective } from '../../directives/stop-propagation/stop-propagation.directive';
|
|
3
7
|
|
|
4
8
|
@Component({
|
|
5
9
|
selector: 'app-datagrid',
|
|
6
10
|
standalone: true,
|
|
11
|
+
imports: [HoverHighlightDirective, StopPropagationDirective, DisabledOpacityDirective, ScrollResetDirective],
|
|
7
12
|
templateUrl: './datagrid.component.html',
|
|
8
13
|
})
|
|
9
14
|
export class DatagridComponent {
|
|
10
15
|
readonly db = inject(DatabaseService);
|
|
11
16
|
private messageService = inject(MessageService);
|
|
12
|
-
private el = inject(ElementRef);
|
|
13
17
|
|
|
14
18
|
readonly sortableColumns = [
|
|
15
19
|
{ field: 'full_name', label: 'Name' },
|
|
@@ -21,14 +25,6 @@ export class DatagridComponent {
|
|
|
21
25
|
{ field: 'designer', label: 'Designer' },
|
|
22
26
|
];
|
|
23
27
|
|
|
24
|
-
constructor() {
|
|
25
|
-
effect(() => {
|
|
26
|
-
this.db.currentPage();
|
|
27
|
-
const scrollable = this.el.nativeElement.querySelector('.scrollbox-y');
|
|
28
|
-
scrollable?.scrollTo(0, 0);
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
selectStore(id: number) {
|
|
33
29
|
this.db.storeId.set(id);
|
|
34
30
|
document.getElementById('preview-store-' + id)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
<button
|
|
18
18
|
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
19
19
|
[style.color]="'var(--text-secondary)'"
|
|
20
|
-
[
|
|
21
|
-
[class.opacity-30]="currentPage() === 1"
|
|
20
|
+
[appDisabledOpacity]="currentPage() === 1"
|
|
22
21
|
(click)="currentPage.set(1)"
|
|
23
22
|
>
|
|
24
23
|
<span
|
|
@@ -34,8 +33,7 @@
|
|
|
34
33
|
<button
|
|
35
34
|
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
36
35
|
[style.color]="'var(--text-secondary)'"
|
|
37
|
-
[
|
|
38
|
-
[class.opacity-30]="currentPage() === 1"
|
|
36
|
+
[appDisabledOpacity]="currentPage() === 1"
|
|
39
37
|
(click)="currentPage.set(currentPage() - 1)"
|
|
40
38
|
>
|
|
41
39
|
<span
|
|
@@ -52,8 +50,7 @@
|
|
|
52
50
|
<button
|
|
53
51
|
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
54
52
|
[style.color]="'var(--text-secondary)'"
|
|
55
|
-
[
|
|
56
|
-
[class.opacity-30]="currentPage() === totalPages()"
|
|
53
|
+
[appDisabledOpacity]="currentPage() === totalPages()"
|
|
57
54
|
(click)="currentPage.set(currentPage() + 1)"
|
|
58
55
|
>
|
|
59
56
|
<span
|
|
@@ -69,8 +66,7 @@
|
|
|
69
66
|
<button
|
|
70
67
|
class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
|
|
71
68
|
[style.color]="'var(--text-secondary)'"
|
|
72
|
-
[
|
|
73
|
-
[class.opacity-30]="currentPage() === totalPages()"
|
|
69
|
+
[appDisabledOpacity]="currentPage() === totalPages()"
|
|
74
70
|
(click)="currentPage.set(totalPages())"
|
|
75
71
|
>
|
|
76
72
|
<span
|
|
@@ -117,16 +113,10 @@
|
|
|
117
113
|
<div
|
|
118
114
|
class="flex flex-col items-center justify-center rounded aspect-square cursor-pointer transition-colors"
|
|
119
115
|
[style.border]="selectedGlyph() === codePoint ? '1px solid var(--selected-border)' : '1px solid var(--border-subtle)'"
|
|
120
|
-
[
|
|
116
|
+
[appHoverHighlight]="selectedGlyph() === codePoint"
|
|
121
117
|
[title]="'U+' + codePoint.toString(16).toUpperCase().padStart(4, '0')"
|
|
122
118
|
[style.font-family]="fontFamily()"
|
|
123
119
|
(click)="selectedGlyph.set(codePoint)"
|
|
124
|
-
(mouseenter)="
|
|
125
|
-
$any($event.currentTarget).style.backgroundColor = selectedGlyph() === codePoint ? 'var(--selected-bg)' : 'var(--hover-bg)'
|
|
126
|
-
"
|
|
127
|
-
(mouseleave)="
|
|
128
|
-
$any($event.currentTarget).style.backgroundColor = selectedGlyph() === codePoint ? 'var(--selected-bg)' : 'transparent'
|
|
129
|
-
"
|
|
130
120
|
>
|
|
131
121
|
<span class="text-lg leading-none" [style.color]="'var(--text-primary)'">{{ toChar(codePoint) }}</span>
|
|
132
122
|
</div>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Component, inject, signal, computed, effect } from '@angular/core';
|
|
2
2
|
import { DatabaseService, PresentationService } from '../../../core/services';
|
|
3
|
+
import { DisabledOpacityDirective } from '../../directives/disabled-opacity/disabled-opacity.directive';
|
|
4
|
+
import { HoverHighlightDirective } from '../../directives/hover-highlight/hover-highlight.directive';
|
|
3
5
|
|
|
4
6
|
const PAGE_SIZE = 200;
|
|
5
7
|
|
|
6
8
|
@Component({
|
|
7
9
|
selector: 'app-glyphs',
|
|
8
10
|
standalone: true,
|
|
11
|
+
imports: [HoverHighlightDirective, DisabledOpacityDirective],
|
|
9
12
|
templateUrl: './glyphs.component.html',
|
|
10
13
|
})
|
|
11
14
|
export class GlyphsComponent {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="panel">
|
|
2
|
-
<div class="h-full scrollbox-y flex flex-col gap-3 p-4">
|
|
2
|
+
<div class="h-full scrollbox-y flex flex-col gap-3 p-4" [appScrollReset]="db.currentPage()">
|
|
3
3
|
@for (store of db.stores(); track store.id; let last = $last) {
|
|
4
4
|
<div
|
|
5
5
|
[class.border-b]="!last"
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { Component, inject, effect
|
|
1
|
+
import { Component, inject, effect } from '@angular/core';
|
|
2
2
|
import { DatabaseService, MessageService, PresentationService } from '../../../core/services';
|
|
3
|
+
import { ScrollResetDirective } from '../../directives/scroll-reset/scroll-reset.directive';
|
|
3
4
|
|
|
4
5
|
@Component({
|
|
5
6
|
selector: 'app-preview',
|
|
6
7
|
standalone: true,
|
|
8
|
+
imports: [ScrollResetDirective],
|
|
7
9
|
templateUrl: './preview.component.html',
|
|
8
10
|
})
|
|
9
11
|
export class PreviewComponent {
|
|
10
12
|
readonly db = inject(DatabaseService);
|
|
11
13
|
readonly presentation = inject(PresentationService);
|
|
12
|
-
private el = inject(ElementRef);
|
|
13
14
|
private messageService = inject(MessageService);
|
|
14
15
|
|
|
15
16
|
private registeredFonts = new Set<string>();
|
|
@@ -21,12 +22,6 @@ export class PreviewComponent {
|
|
|
21
22
|
stores.forEach((store) => this.registerFont(store));
|
|
22
23
|
}
|
|
23
24
|
});
|
|
24
|
-
|
|
25
|
-
effect(() => {
|
|
26
|
-
this.db.currentPage();
|
|
27
|
-
const scrollable = this.el.nativeElement.querySelector('.scrollbox-y');
|
|
28
|
-
scrollable?.scrollTo(0, 0);
|
|
29
|
-
});
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
selectStore(id: number) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" (
|
|
2
|
-
<div class="card mb-0 w-[480px]" style="box-shadow: var(--context-shadow)"
|
|
1
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" appModalBackdrop (backdropClick)="cancel()">
|
|
2
|
+
<div class="card mb-0 w-[480px]" style="box-shadow: var(--context-shadow)">
|
|
3
3
|
<div class="card-header">
|
|
4
4
|
<div class="card-title">
|
|
5
5
|
<h3>{{ title }}</h3>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Component, Input, Output, EventEmitter, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
|
|
2
2
|
import { FormsModule } from '@angular/forms';
|
|
3
|
+
import { ModalBackdropDirective } from '../../directives/modal-backdrop/modal-backdrop.directive';
|
|
3
4
|
|
|
4
5
|
@Component({
|
|
5
6
|
selector: 'app-prompt-dialog',
|
|
6
7
|
standalone: true,
|
|
7
|
-
imports: [FormsModule],
|
|
8
|
+
imports: [FormsModule, ModalBackdropDirective],
|
|
8
9
|
templateUrl: './prompt-dialog.component.html',
|
|
9
10
|
})
|
|
10
11
|
export class PromptDialogComponent implements AfterViewInit {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" (
|
|
2
|
-
<div class="card mb-0 w-[560px]" style="box-shadow: var(--context-shadow)"
|
|
1
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" appModalBackdrop (backdropClick)="cancel()">
|
|
2
|
+
<div class="card mb-0 w-[560px]" style="box-shadow: var(--context-shadow)">
|
|
3
3
|
<div class="card-header">
|
|
4
4
|
<div class="card-title">
|
|
5
5
|
<h3>{{ smartCollection ? 'Edit Smart Collection' : 'New Smart Collection' }}</h3>
|
|
@@ -78,15 +78,27 @@
|
|
|
78
78
|
}
|
|
79
79
|
</div>
|
|
80
80
|
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
<div class="flex items-center justify-between">
|
|
82
|
+
<button class="btn btn-sm btn-default" (click)="addRule()">
|
|
83
|
+
<span class="material-symbols-outlined" style="font-size: 16px; vertical-align: middle">add</span>
|
|
84
|
+
Add Rule
|
|
85
|
+
</button>
|
|
86
|
+
<div class="flex items-center gap-2">
|
|
87
|
+
@if (previewCount !== null) {
|
|
88
|
+
<span class="text-xs opacity-70">{{ previewCount }} {{ previewCount === 1 ? 'font' : 'fonts' }} matched</span>
|
|
89
|
+
}
|
|
90
|
+
<button class="btn btn-sm btn-default" (click)="preview()" [disabled]="previewing">
|
|
91
|
+
<span class="material-symbols-outlined" style="font-size: 16px; vertical-align: middle">search</span>
|
|
92
|
+
{{ previewing ? 'Checking...' : 'Preview' }}
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
85
96
|
</div>
|
|
86
97
|
<div class="card-footer">
|
|
87
98
|
<div></div>
|
|
88
99
|
<div class="card-tools">
|
|
89
100
|
<button class="btn btn-sm btn-default" (click)="cancel()">Cancel</button>
|
|
101
|
+
<button class="btn btn-sm btn-default" (click)="saveAndSync()">Save & Sync</button>
|
|
90
102
|
<button class="btn btn-sm btn-theme" (click)="save()">Save</button>
|
|
91
103
|
</div>
|
|
92
104
|
</div>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
|
1
|
+
import { Component, Input, Output, EventEmitter, OnInit, inject } from '@angular/core';
|
|
2
2
|
import { FormsModule } from '@angular/forms';
|
|
3
|
+
import { ModalBackdropDirective } from '../../directives/modal-backdrop/modal-backdrop.directive';
|
|
4
|
+
import { DatabaseService } from '../../../core/services/database/database.service';
|
|
3
5
|
import type { SmartCollection } from '@main/database/entity/SmartCollection.schema';
|
|
4
6
|
import type { SmartCollectionRule } from '@main/types';
|
|
5
7
|
|
|
@@ -65,17 +67,22 @@ const OPERATORS_BY_TYPE: Record<string, { value: string; label: string }[]> = {
|
|
|
65
67
|
@Component({
|
|
66
68
|
selector: 'app-rule-builder',
|
|
67
69
|
standalone: true,
|
|
68
|
-
imports: [FormsModule],
|
|
70
|
+
imports: [FormsModule, ModalBackdropDirective],
|
|
69
71
|
templateUrl: './rule-builder.component.html',
|
|
70
72
|
})
|
|
71
73
|
export class RuleBuilderComponent implements OnInit {
|
|
72
74
|
@Input() smartCollection: SmartCollection | null = null;
|
|
73
75
|
@Output() saved = new EventEmitter<{ title: string; rules: SmartCollectionRule[]; match_type: string }>();
|
|
76
|
+
@Output() savedAndSync = new EventEmitter<{ title: string; rules: SmartCollectionRule[]; match_type: string }>();
|
|
74
77
|
@Output() cancelled = new EventEmitter<void>();
|
|
75
78
|
|
|
79
|
+
private db = inject(DatabaseService);
|
|
80
|
+
|
|
76
81
|
title = '';
|
|
77
82
|
matchType: string = 'AND';
|
|
78
83
|
rules: SmartCollectionRule[] = [];
|
|
84
|
+
previewCount: number | null = null;
|
|
85
|
+
previewing = false;
|
|
79
86
|
|
|
80
87
|
readonly fieldOptions = FIELD_OPTIONS;
|
|
81
88
|
|
|
@@ -114,14 +121,33 @@ export class RuleBuilderComponent implements OnInit {
|
|
|
114
121
|
} else {
|
|
115
122
|
rule.value = '';
|
|
116
123
|
}
|
|
124
|
+
this.previewCount = null;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
addRule(): void {
|
|
120
128
|
this.rules.push({ field: 'font_family', operator: 'contains', value: '' });
|
|
129
|
+
this.previewCount = null;
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
removeRule(index: number): void {
|
|
124
133
|
this.rules.splice(index, 1);
|
|
134
|
+
this.previewCount = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
preview(): void {
|
|
138
|
+
if (this.rules.length === 0 || this.previewing) return;
|
|
139
|
+
this.previewing = true;
|
|
140
|
+
this.previewCount = null;
|
|
141
|
+
this.db
|
|
142
|
+
.smartCollectionPreview(this.rules, this.matchType)
|
|
143
|
+
.then((count) => {
|
|
144
|
+
this.previewCount = count;
|
|
145
|
+
this.previewing = false;
|
|
146
|
+
})
|
|
147
|
+
.catch(() => {
|
|
148
|
+
this.previewCount = null;
|
|
149
|
+
this.previewing = false;
|
|
150
|
+
});
|
|
125
151
|
}
|
|
126
152
|
|
|
127
153
|
save(): void {
|
|
@@ -130,6 +156,12 @@ export class RuleBuilderComponent implements OnInit {
|
|
|
130
156
|
this.saved.emit({ title: trimmedTitle, rules: this.rules, match_type: this.matchType });
|
|
131
157
|
}
|
|
132
158
|
|
|
159
|
+
saveAndSync(): void {
|
|
160
|
+
const trimmedTitle = this.title.trim();
|
|
161
|
+
if (!trimmedTitle || this.rules.length === 0) return;
|
|
162
|
+
this.savedAndSync.emit({ title: trimmedTitle, rules: this.rules, match_type: this.matchType });
|
|
163
|
+
}
|
|
164
|
+
|
|
133
165
|
cancel(): void {
|
|
134
166
|
this.cancelled.emit();
|
|
135
167
|
}
|