fontastic 1.0.0 → 1.0.1

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.
Files changed (47) hide show
  1. package/.github/workflows/macos.yml +0 -7
  2. package/.github/workflows/ubuntu.yml +0 -7
  3. package/.github/workflows/windows.yml +0 -7
  4. package/README.md +1 -0
  5. package/app/core/ConnectionManager.js +7 -1
  6. package/app/core/ConnectionManager.ts +11 -3
  7. package/app/core/MessageHandler.js +24 -0
  8. package/app/core/MessageHandler.ts +29 -0
  9. package/app/core/menu/templates/DarwinTemplate.js +15 -0
  10. package/app/core/menu/templates/DarwinTemplate.ts +15 -0
  11. package/app/core/menu/templates/SystemTemplate.js +15 -0
  12. package/app/core/menu/templates/SystemTemplate.ts +15 -0
  13. package/app/database/entity/SmartCollection.schema.js +66 -0
  14. package/app/database/entity/SmartCollection.schema.ts +39 -0
  15. package/app/database/entity/index.js +1 -0
  16. package/app/database/entity/index.ts +2 -1
  17. package/app/database/repository/SmartCollection.repository.js +47 -0
  18. package/app/database/repository/SmartCollection.repository.ts +30 -0
  19. package/app/database/repository/Store.repository.js +107 -0
  20. package/app/database/repository/Store.repository.ts +106 -0
  21. package/app/database/repository/index.js +1 -0
  22. package/app/database/repository/index.ts +2 -1
  23. package/app/enums/ChannelType.js +5 -0
  24. package/app/enums/ChannelType.ts +6 -0
  25. package/app/enums/StorageType.js +1 -0
  26. package/app/enums/StorageType.ts +1 -0
  27. package/app/package.json +1 -1
  28. package/app/types/FontMetrics.js +3 -0
  29. package/app/types/SmartCollection.js +3 -0
  30. package/app/types/SmartCollection.ts +5 -0
  31. package/app/types/index.js +2 -0
  32. package/app/types/index.ts +1 -0
  33. package/package.json +1 -1
  34. package/src/app/core/services/database/database.service.ts +70 -1
  35. package/src/app/core/services/message/message.service.ts +23 -0
  36. package/src/app/core/services/presentation/presentation.service.ts +47 -0
  37. package/src/app/layout/header/header.component.html +1 -1
  38. package/src/app/layout/layout.component.html +1 -1
  39. package/src/app/layout/navigation/navigation.component.html +70 -4
  40. package/src/app/layout/navigation/navigation.component.ts +129 -25
  41. package/src/app/shared/components/datagrid/datagrid.component.html +82 -2
  42. package/src/app/shared/components/index.ts +1 -0
  43. package/src/app/shared/components/rule-builder/rule-builder.component.html +94 -0
  44. package/src/app/shared/components/rule-builder/rule-builder.component.ts +136 -0
  45. package/src/app/shared/components/toolbar/toolbar.component.html +0 -81
  46. package/src/app/shared/components/toolbar/toolbar.component.ts +1 -2
  47. package/src/styles/base/variables.css +16 -0
@@ -1,6 +1,7 @@
1
1
  import { Brackets, Equal } from 'typeorm';
2
2
  import { Store } from '../entity';
3
3
  import { searchDbColumns } from '../../config';
4
+ import type { SmartCollectionRule } from '../../types';
4
5
 
5
6
  export const StoreRepository = {
6
7
  async search(options: any) {
@@ -330,6 +331,111 @@ export const StoreRepository = {
330
331
  .catch((err: any) => console.log('insert-error', err));
331
332
  },
332
333
 
334
+ async evaluateSmartRules(rules: SmartCollectionRule[], matchType: string, options: any = {}) {
335
+ const db = this.createQueryBuilder();
336
+
337
+ const textFields = [
338
+ 'file_name',
339
+ 'file_path',
340
+ 'compatible_full_name',
341
+ 'copyright',
342
+ 'description',
343
+ 'designer',
344
+ 'designer_url',
345
+ 'font_family',
346
+ 'font_subfamily',
347
+ 'full_name',
348
+ 'license',
349
+ 'license_url',
350
+ 'manufacturer',
351
+ 'manufacturer_url',
352
+ 'post_script_name',
353
+ 'preferred_family',
354
+ 'preferred_sub_family',
355
+ 'sample_text',
356
+ 'trademark',
357
+ 'unique_id',
358
+ 'version',
359
+ ];
360
+ const booleanFields = ['favorite', 'system', 'installable', 'temporary'];
361
+ const numericFields = ['file_size'];
362
+ const dateFields = ['created'];
363
+
364
+ if (rules.length > 0) {
365
+ const conditions: ((qb: any) => void)[] = rules.map((rule, idx) => {
366
+ const field = `store.${rule.field}`;
367
+ const paramName = `p${idx}`;
368
+
369
+ return (qb: any) => {
370
+ if (textFields.includes(rule.field)) {
371
+ const val = String(rule.value).toLowerCase();
372
+ switch (rule.operator) {
373
+ case 'contains':
374
+ qb.where(`LOWER(${field}) LIKE :${paramName}`, { [paramName]: `%${val}%` });
375
+ break;
376
+ case 'equals':
377
+ qb.where(`LOWER(${field}) = :${paramName}`, { [paramName]: val });
378
+ break;
379
+ case 'starts_with':
380
+ qb.where(`LOWER(${field}) LIKE :${paramName}`, { [paramName]: `${val}%` });
381
+ break;
382
+ case 'ends_with':
383
+ qb.where(`LOWER(${field}) LIKE :${paramName}`, { [paramName]: `%${val}` });
384
+ break;
385
+ default:
386
+ qb.where(`LOWER(${field}) LIKE :${paramName}`, { [paramName]: `%${val}%` });
387
+ }
388
+ } else if (rule.field === 'file_type') {
389
+ qb.where(`${field} = :${paramName}`, { [paramName]: rule.value });
390
+ } else if (booleanFields.includes(rule.field)) {
391
+ const boolVal = rule.operator === 'is_not' ? 0 : 1;
392
+ qb.where(`${field} = :${paramName}`, { [paramName]: boolVal });
393
+ } else if (numericFields.includes(rule.field)) {
394
+ if (rule.operator === 'greater_than') {
395
+ qb.where(`${field} > :${paramName}`, { [paramName]: rule.value });
396
+ } else if (rule.operator === 'less_than') {
397
+ qb.where(`${field} < :${paramName}`, { [paramName]: rule.value });
398
+ } else {
399
+ qb.where(`${field} = :${paramName}`, { [paramName]: rule.value });
400
+ }
401
+ } else if (dateFields.includes(rule.field)) {
402
+ if (rule.operator === 'greater_than') {
403
+ qb.where(`${field} >= :${paramName}`, { [paramName]: rule.value });
404
+ } else if (rule.operator === 'less_than') {
405
+ qb.where(`${field} <= :${paramName}`, { [paramName]: rule.value });
406
+ } else {
407
+ qb.where(`${field} = :${paramName}`, { [paramName]: rule.value });
408
+ }
409
+ }
410
+ };
411
+ });
412
+
413
+ const joiner = matchType === 'OR' ? 'orWhere' : 'andWhere';
414
+ conditions.forEach((condition, i) => {
415
+ if (i === 0) {
416
+ db.where(new Brackets(condition));
417
+ } else {
418
+ db[joiner](new Brackets(condition));
419
+ }
420
+ });
421
+ }
422
+
423
+ if (options.take) {
424
+ db.limit(options.take);
425
+ }
426
+
427
+ if (options.skip) {
428
+ db.offset(options.skip);
429
+ }
430
+
431
+ if (options.order && options.order.column) {
432
+ const direction = options.order.direction === 'DESC' ? 'DESC' : 'ASC';
433
+ db.orderBy(`store.${options.order.column}`, direction);
434
+ }
435
+
436
+ return await db.getManyAndCount();
437
+ },
438
+
333
439
  async fetchSystemStats() {
334
440
  const rowCount = await this.createQueryBuilder().select('COUNT(*)', 'total').getRawOne();
335
441
 
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./Collection.repository"), exports);
18
+ __exportStar(require("./SmartCollection.repository"), exports);
18
19
  __exportStar(require("./Store.repository"), exports);
19
20
  __exportStar(require("./Logger.repository"), exports);
20
21
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,4 @@
1
1
  export * from './Collection.repository';
2
+ export * from './SmartCollection.repository';
2
3
  export * from './Store.repository';
3
- export * from './Logger.repository';
4
+ export * from './Logger.repository';
@@ -58,6 +58,11 @@ var ChannelType;
58
58
  ChannelType["IPC_LOGGER_DELETE"] = "IPC_LOGGER_DELETE";
59
59
  ChannelType["IPC_LOGGER_QUERY"] = "IPC_LOGGER_QUERY";
60
60
  ChannelType["IPC_LOGGER_TRUNCATE"] = "IPC_LOGGER_TRUNCATE";
61
+ ChannelType["IPC_SMART_COLLECTION_FIND"] = "IPC_SMART_COLLECTION_FIND";
62
+ ChannelType["IPC_SMART_COLLECTION_CREATE"] = "IPC_SMART_COLLECTION_CREATE";
63
+ ChannelType["IPC_SMART_COLLECTION_UPDATE"] = "IPC_SMART_COLLECTION_UPDATE";
64
+ ChannelType["IPC_SMART_COLLECTION_DELETE"] = "IPC_SMART_COLLECTION_DELETE";
65
+ ChannelType["IPC_SMART_COLLECTION_EVALUATE"] = "IPC_SMART_COLLECTION_EVALUATE";
61
66
  ChannelType["IPC_TOGGLE_PANEL"] = "IPC_TOGGLE_PANEL";
62
67
  })(ChannelType || (exports.ChannelType = ChannelType = {}));
63
68
  //# sourceMappingURL=ChannelType.js.map
@@ -63,5 +63,11 @@ export enum ChannelType {
63
63
  IPC_LOGGER_QUERY = 'IPC_LOGGER_QUERY',
64
64
  IPC_LOGGER_TRUNCATE = 'IPC_LOGGER_TRUNCATE',
65
65
 
66
+ IPC_SMART_COLLECTION_FIND = 'IPC_SMART_COLLECTION_FIND',
67
+ IPC_SMART_COLLECTION_CREATE = 'IPC_SMART_COLLECTION_CREATE',
68
+ IPC_SMART_COLLECTION_UPDATE = 'IPC_SMART_COLLECTION_UPDATE',
69
+ IPC_SMART_COLLECTION_DELETE = 'IPC_SMART_COLLECTION_DELETE',
70
+ IPC_SMART_COLLECTION_EVALUATE = 'IPC_SMART_COLLECTION_EVALUATE',
71
+
66
72
  IPC_TOGGLE_PANEL = 'IPC_TOGGLE_PANEL',
67
73
  }
@@ -16,5 +16,6 @@ var StorageType;
16
16
  StorageType["LayoutPreview"] = "layout.preview";
17
17
  StorageType["LayoutTheme"] = "layout.theme";
18
18
  StorageType["AiKeys"] = "ai.keys";
19
+ StorageType["NavigationExpanded"] = "navigation.expanded";
19
20
  })(StorageType || (exports.StorageType = StorageType = {}));
20
21
  //# sourceMappingURL=StorageType.js.map
@@ -12,4 +12,5 @@ export enum StorageType {
12
12
  LayoutPreview = 'layout.preview',
13
13
  LayoutTheme = 'layout.theme',
14
14
  AiKeys = 'ai.keys',
15
+ NavigationExpanded = 'navigation.expanded',
15
16
  }
package/app/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Tom Shaw",
6
6
  "email": ""
7
7
  },
8
- "version": "1.0.0",
8
+ "version": "1.0.1",
9
9
  "main": "main.js",
10
10
  "private": true,
11
11
  "dependencies": {
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=FontMetrics.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=SmartCollection.js.map
@@ -0,0 +1,5 @@
1
+ export interface SmartCollectionRule {
2
+ field: string;
3
+ operator: string;
4
+ value: string | number;
5
+ }
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./AppAlert"), exports);
18
+ __exportStar(require("./FontMetrics"), exports);
18
19
  __exportStar(require("./AuthUser"), exports);
19
20
  __exportStar(require("./Breadcrumb"), exports);
20
21
  __exportStar(require("./QueryOptions"), exports);
@@ -22,4 +23,5 @@ __exportStar(require("./SystemTheme"), exports);
22
23
  __exportStar(require("./SystemStats"), exports);
23
24
  __exportStar(require("./SystemConfig"), exports);
24
25
  __exportStar(require("./ImportOptions"), exports);
26
+ __exportStar(require("./SmartCollection"), exports);
25
27
  //# sourceMappingURL=index.js.map
@@ -7,3 +7,4 @@ export * from './SystemTheme';
7
7
  export * from './SystemStats';
8
8
  export * from './SystemConfig';
9
9
  export * from './ImportOptions';
10
+ export * from './SmartCollection';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontastic",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A gorgeous multi-platform font management application.",
5
5
  "homepage": "https://github.com/tomshaw/fontastic",
6
6
  "private": false,
@@ -7,6 +7,7 @@ import type { Collection } from '@main/database/entity/Collection.schema';
7
7
  import type { Logger } from '@main/database/entity/Logger.schema';
8
8
  import type { Store, StoreManyAndCountType } from '@main/database/entity/Store.schema';
9
9
  import type { FontMetrics, SystemStats } from '@main/types';
10
+ import type { SmartCollection } from '@main/database/entity/SmartCollection.schema';
10
11
 
11
12
  @Injectable({
12
13
  providedIn: 'root',
@@ -18,6 +19,8 @@ export class DatabaseService {
18
19
 
19
20
  // Reactive state
20
21
  readonly collections = signal<Collection[]>([]);
22
+ readonly smartCollections = signal<SmartCollection[]>([]);
23
+ readonly activeSmartCollectionId = signal<number | null>(null);
21
24
  readonly parentId = signal<number | null>(null);
22
25
  readonly collectionId = signal<number | null>(null);
23
26
  readonly stores = signal<Store[]>([]);
@@ -56,6 +59,7 @@ export class DatabaseService {
56
59
  this.collectionId.set(parentId);
57
60
  this.activeFilter.set(null);
58
61
  this.activeSearchWhere.set(null);
62
+ this.activeSmartCollectionId.set(null);
59
63
  this.currentPage.set(1);
60
64
  }
61
65
 
@@ -64,6 +68,7 @@ export class DatabaseService {
64
68
  this.collectionId.set(child.id);
65
69
  this.activeFilter.set(null);
66
70
  this.activeSearchWhere.set(null);
71
+ this.activeSmartCollectionId.set(null);
67
72
  this.currentPage.set(1);
68
73
  }
69
74
 
@@ -78,11 +83,22 @@ export class DatabaseService {
78
83
  return current.id;
79
84
  }
80
85
 
86
+ selectSmartCollection(id: number) {
87
+ this.parentId.set(null);
88
+ this.collectionId.set(null);
89
+ this.activeFilter.set(null);
90
+ this.activeSearchWhere.set(null);
91
+ this.activeSmartCollectionId.set(id);
92
+ this.currentPage.set(1);
93
+ this.fetchCurrentPage();
94
+ }
95
+
81
96
  selectFilter(filter: string) {
82
97
  this.parentId.set(null);
83
98
  this.collectionId.set(null);
84
99
  this.activeFilter.set(filter);
85
100
  this.activeSearchWhere.set(null);
101
+ this.activeSmartCollectionId.set(null);
86
102
  this.currentPage.set(1);
87
103
 
88
104
  const whereMap: Record<string, { key: string; value: number }[]> = {
@@ -122,6 +138,12 @@ export class DatabaseService {
122
138
  const skip = (this.currentPage() - 1) * this.pageSize();
123
139
  const take = this.pageSize();
124
140
 
141
+ const smartCollectionId = this.activeSmartCollectionId();
142
+ if (smartCollectionId) {
143
+ this.smartCollectionEvaluate(smartCollectionId, { skip, take });
144
+ return;
145
+ }
146
+
125
147
  const searchWhere = this.activeSearchWhere();
126
148
  if (searchWhere) {
127
149
  const searchOrder = this.activeSearchOrder();
@@ -150,13 +172,15 @@ export class DatabaseService {
150
172
 
151
173
  constructor() {
152
174
  this.electron.ready.then(async () => {
153
- const [collections, savedCollectionId, savedStoreId] = await Promise.all([
175
+ const [collections, smartCollections, savedCollectionId, savedStoreId] = await Promise.all([
154
176
  this.message.collectionFetch({}),
177
+ this.message.smartCollectionFind(),
155
178
  this.message.get(StorageType.CollectionId, null),
156
179
  this.message.get(StorageType.StoreId, null),
157
180
  ]);
158
181
 
159
182
  this.collections.set(collections);
183
+ this.smartCollections.set(smartCollections);
160
184
  console.log('System Boot:', collections);
161
185
 
162
186
  if (savedCollectionId) {
@@ -294,6 +318,50 @@ export class DatabaseService {
294
318
  );
295
319
  }
296
320
 
321
+ // Smart Collection
322
+
323
+ smartCollectionCreate(args: any): Promise<SmartCollection[]> {
324
+ return this.track(
325
+ this.message.smartCollectionCreate(args).then((result) => {
326
+ this.smartCollections.set(result);
327
+ return result;
328
+ }),
329
+ );
330
+ }
331
+
332
+ smartCollectionUpdate(id: number, data: any): Promise<SmartCollection[]> {
333
+ return this.track(
334
+ this.message.smartCollectionUpdate(id, data).then((result) => {
335
+ this.smartCollections.set(result);
336
+ return result;
337
+ }),
338
+ );
339
+ }
340
+
341
+ smartCollectionDelete(id: number): Promise<SmartCollection[]> {
342
+ return this.track(
343
+ this.message.smartCollectionDelete(id).then((result) => {
344
+ this.smartCollections.set(result);
345
+ if (this.activeSmartCollectionId() === id) {
346
+ this.activeSmartCollectionId.set(null);
347
+ this.stores.set([]);
348
+ this.storeCount.set(0);
349
+ }
350
+ return result;
351
+ }),
352
+ );
353
+ }
354
+
355
+ smartCollectionEvaluate(id: number, options: any): Promise<StoreManyAndCountType> {
356
+ return this.track(
357
+ this.message.smartCollectionEvaluate(id, options).then((result) => {
358
+ this.stores.set(result[0] as Store[]);
359
+ this.storeCount.set(result[1] as number);
360
+ return result;
361
+ }),
362
+ );
363
+ }
364
+
297
365
  // Store
298
366
 
299
367
  storeFind(args: any): Promise<Store[]> {
@@ -336,6 +404,7 @@ export class DatabaseService {
336
404
  this.activeFilter.set(null);
337
405
  this.activeSearchWhere.set(where);
338
406
  this.activeSearchOrder.set(order ?? null);
407
+ this.activeSmartCollectionId.set(null);
339
408
  this.currentPage.set(1);
340
409
  this.fetchCurrentPage();
341
410
  }
@@ -5,6 +5,7 @@ import type { Logger } from '@main/database/entity/Logger.schema';
5
5
  import type { Store, StoreManyAndCountType } from '@main/database/entity/Store.schema';
6
6
  import { ChannelType } from '@main/enums';
7
7
  import type { FontMetrics, SystemConfig, SystemStats } from '@main/types';
8
+ import type { SmartCollection } from '@main/database/entity/SmartCollection.schema';
8
9
 
9
10
  @Injectable({
10
11
  providedIn: 'root',
@@ -213,6 +214,28 @@ export class MessageService {
213
214
  return this.invoke<Collection[]>(ChannelType.IPC_COLLECTION_MOVE, { collectionId, newParentId, newIndex });
214
215
  }
215
216
 
217
+ // Smart Collection
218
+
219
+ smartCollectionFind(): Promise<SmartCollection[]> {
220
+ return this.invoke<SmartCollection[]>(ChannelType.IPC_SMART_COLLECTION_FIND);
221
+ }
222
+
223
+ smartCollectionCreate(args: any): Promise<SmartCollection[]> {
224
+ return this.invoke<SmartCollection[]>(ChannelType.IPC_SMART_COLLECTION_CREATE, args);
225
+ }
226
+
227
+ smartCollectionUpdate(id: number, data: any): Promise<SmartCollection[]> {
228
+ return this.invoke<SmartCollection[]>(ChannelType.IPC_SMART_COLLECTION_UPDATE, { id, data });
229
+ }
230
+
231
+ smartCollectionDelete(id: number): Promise<SmartCollection[]> {
232
+ return this.invoke<SmartCollection[]>(ChannelType.IPC_SMART_COLLECTION_DELETE, { id });
233
+ }
234
+
235
+ smartCollectionEvaluate(id: number, options: any): Promise<StoreManyAndCountType> {
236
+ return this.invoke<StoreManyAndCountType>(ChannelType.IPC_SMART_COLLECTION_EVALUATE, { id, ...options });
237
+ }
238
+
216
239
  // Store
217
240
 
218
241
  storeFind(args: any): Promise<Store[]> {
@@ -41,6 +41,7 @@ export class PresentationService {
41
41
  this.loadThemeSettings();
42
42
  this.loadLayoutSettings();
43
43
  this.loadPreviewSettings();
44
+ this.loadNavigationExpandedSettings();
44
45
  this.listenForMenuToggle();
45
46
 
46
47
  let themeInitialized = false;
@@ -70,6 +71,15 @@ export class PresentationService {
70
71
  panelInitialized = true;
71
72
  });
72
73
 
74
+ let navExpandedInitialized = false;
75
+ effect(() => {
76
+ const ids = this.navigationExpandedIds();
77
+ if (navExpandedInitialized) {
78
+ untracked(() => this.messageService.set(StorageType.NavigationExpanded, ids));
79
+ }
80
+ navExpandedInitialized = true;
81
+ });
82
+
73
83
  let previewInitialized = false;
74
84
  effect(() => {
75
85
  const settings: LayoutPreviewType = {
@@ -120,6 +130,8 @@ export class PresentationService {
120
130
  readonly letterSpacing = signal(0);
121
131
  readonly wordSpacing = signal(0);
122
132
 
133
+ readonly navigationExpandedIds = signal<number[]>([]);
134
+
123
135
  readonly gridEnabled = signal(true);
124
136
  readonly toolbarEnabled = signal(true);
125
137
  readonly previewEnabled = signal(true);
@@ -191,6 +203,41 @@ export class PresentationService {
191
203
  });
192
204
  }
193
205
 
206
+ isNavigationExpanded(id: number): boolean {
207
+ return this.navigationExpandedIds().includes(id);
208
+ }
209
+
210
+ toggleNavigationExpanded(id: number) {
211
+ const ids = this.navigationExpandedIds();
212
+ if (ids.includes(id)) {
213
+ this.navigationExpandedIds.set(ids.filter((i) => i !== id));
214
+ } else {
215
+ this.navigationExpandedIds.set([...ids, id]);
216
+ }
217
+ }
218
+
219
+ expandNavigationId(id: number) {
220
+ const ids = this.navigationExpandedIds();
221
+ if (!ids.includes(id)) {
222
+ this.navigationExpandedIds.set([...ids, id]);
223
+ }
224
+ }
225
+
226
+ setAllNavigationExpanded(allIds: number[]) {
227
+ this.navigationExpandedIds.set(allIds);
228
+ }
229
+
230
+ clearAllNavigationExpanded() {
231
+ this.navigationExpandedIds.set([]);
232
+ }
233
+
234
+ private async loadNavigationExpandedSettings() {
235
+ const ids = (await this.messageService.get(StorageType.NavigationExpanded, null)) as number[] | null;
236
+ if (Array.isArray(ids)) {
237
+ this.navigationExpandedIds.set(ids);
238
+ }
239
+ }
240
+
194
241
  private async loadThemeSettings() {
195
242
  const settings = (await this.messageService.get(StorageType.LayoutTheme, null)) as { theme: string } | null;
196
243
  if (settings?.theme && PresentationService.themes.includes(settings.theme as (typeof PresentationService.themes)[number])) {
@@ -1,4 +1,4 @@
1
- <header class="grid grid-cols-[220px_1fr_220px] min-[1440px]:grid-cols-[280px_1fr_280px] w-full h-full">
1
+ <header class="grid w-full h-full" [style.grid-template-columns]="'var(--panel-width) 1fr var(--panel-width)'">
2
2
  <div class="relative">
3
3
  <div class="p-0 h-full flex justify-items-start items-center">
4
4
  <a
@@ -7,7 +7,7 @@
7
7
  <div
8
8
  class="grid overflow-hidden"
9
9
  [style.grid-template-columns]="
10
- (presentation.navigationEnabled() ? '280px ' : '') + '1fr' + (presentation.asideEnabled() ? ' 280px' : '')
10
+ (presentation.navigationEnabled() ? 'var(--panel-width) ' : '') + '1fr' + (presentation.asideEnabled() ? ' var(--panel-width)' : '')
11
11
  "
12
12
  >
13
13
  @if (presentation.navigationEnabled()) {
@@ -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 transition-transform duration-200"
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
- [title]="allExpanded ? 'Collapse All' : 'Expand All'"
19
- (click)="toggleExpandAll($event)"
20
- >{{ allExpanded ? 'unfold_less' : 'unfold_more' }}</span
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>