fontastic 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +22 -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 +79 -2
- package/app/main.ts +100 -3
- 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 +1 -1
- 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 +93 -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/navigation/navigation.component.html +11 -9
- package/src/app/layout/navigation/navigation.component.ts +35 -0
- 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
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
'wght' 300;
|
|
17
17
|
"
|
|
18
18
|
title="New Smart Collection"
|
|
19
|
-
|
|
19
|
+
appStopPropagation
|
|
20
|
+
(click)="openCreateSmartCollection()"
|
|
20
21
|
>add</span
|
|
21
22
|
>
|
|
22
23
|
<ul class="flex flex-col py-0.5">
|
|
@@ -24,13 +25,12 @@
|
|
|
24
25
|
<li>
|
|
25
26
|
<a
|
|
26
27
|
class="flex items-center px-3 py-1 text-xs font-normal cursor-pointer transition-colors"
|
|
27
|
-
[
|
|
28
|
-
|
|
28
|
+
[appHoverHighlight]="isSmartSelected(sc)"
|
|
29
|
+
selectedColor="var(--text-primary)"
|
|
30
|
+
normalColor="inherit"
|
|
29
31
|
(click)="selectSmartCollection(sc)"
|
|
30
32
|
(dblclick)="editSmartCollection(sc)"
|
|
31
33
|
(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
34
|
>
|
|
35
35
|
<span
|
|
36
36
|
class="material-symbols-outlined icon-sm mr-1"
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
'wght' 300;
|
|
65
65
|
"
|
|
66
66
|
title="New Collection"
|
|
67
|
-
|
|
67
|
+
appStopPropagation
|
|
68
|
+
(click)="openCreateRootCollection()"
|
|
68
69
|
>add</span
|
|
69
70
|
>
|
|
70
71
|
<ul class="flex-1 overflow-auto flex flex-col py-0.5">
|
|
@@ -90,7 +91,7 @@
|
|
|
90
91
|
(blur)="saveCreating()"
|
|
91
92
|
(keydown.enter)="saveCreating()"
|
|
92
93
|
(keydown.escape)="cancelCreating()"
|
|
93
|
-
|
|
94
|
+
appStopPropagation
|
|
94
95
|
placeholder="Collection name"
|
|
95
96
|
appAutofocus
|
|
96
97
|
/>
|
|
@@ -164,7 +165,7 @@
|
|
|
164
165
|
(blur)="saveEditing(node.collection.id)"
|
|
165
166
|
(keydown.enter)="saveEditing(node.collection.id)"
|
|
166
167
|
(keydown.escape)="cancelEditing()"
|
|
167
|
-
|
|
168
|
+
appStopPropagation
|
|
168
169
|
appAutofocus
|
|
169
170
|
/>
|
|
170
171
|
} @else {
|
|
@@ -195,7 +196,7 @@
|
|
|
195
196
|
(blur)="saveCreating()"
|
|
196
197
|
(keydown.enter)="saveCreating()"
|
|
197
198
|
(keydown.escape)="cancelCreating()"
|
|
198
|
-
|
|
199
|
+
appStopPropagation
|
|
199
200
|
placeholder="Collection name"
|
|
200
201
|
appAutofocus
|
|
201
202
|
/>
|
|
@@ -233,6 +234,7 @@
|
|
|
233
234
|
<app-rule-builder
|
|
234
235
|
[smartCollection]="editingSmartCollection"
|
|
235
236
|
(saved)="onRuleBuilderSaved($event)"
|
|
237
|
+
(savedAndSync)="onRuleBuilderSavedAndSync($event)"
|
|
236
238
|
(cancelled)="onRuleBuilderCancelled()"
|
|
237
239
|
/>
|
|
238
240
|
}
|
|
@@ -6,6 +6,8 @@ import { CollapsiblePanelComponent } from '../../shared/components/collapsible-p
|
|
|
6
6
|
import { ContextMenuComponent, ContextMenuItem } from '../../shared/components/context-menu/context-menu.component';
|
|
7
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';
|
|
@@ -28,6 +30,8 @@ export interface TreeNode {
|
|
|
28
30
|
ContextMenuComponent,
|
|
29
31
|
RuleBuilderComponent,
|
|
30
32
|
AutofocusDirective,
|
|
33
|
+
HoverHighlightDirective,
|
|
34
|
+
StopPropagationDirective,
|
|
31
35
|
LibraryComponent,
|
|
32
36
|
NewsStatsComponent,
|
|
33
37
|
],
|
|
@@ -476,6 +480,37 @@ export class NavigationComponent implements OnInit, OnDestroy {
|
|
|
476
480
|
this.editingSmartCollection = null;
|
|
477
481
|
}
|
|
478
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
|
+
|
|
479
514
|
onRuleBuilderCancelled() {
|
|
480
515
|
this.showRuleBuilder = false;
|
|
481
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
|
}
|