mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Add support for uploading and assigning custom SVG icons to libraries… (#1788)
* Add support for uploading and assigning custom SVG icons to libraries and shelves * Add few default icons
This commit is contained in:
@@ -60,11 +60,20 @@
|
||||
} @else {
|
||||
<div class="selected-icon-display">
|
||||
<div class="icon-preview">
|
||||
<i [class]="selectedIcon"></i>
|
||||
<app-icon-display
|
||||
[icon]="selectedIcon"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
<div class="icon-info">
|
||||
<span class="icon-label">Selected Icon</span>
|
||||
<span class="icon-name">{{ selectedIcon }}</span>
|
||||
<span class="icon-name">
|
||||
@if (selectedIcon.type === 'PRIME_NG') {
|
||||
{{ selectedIcon.value }}
|
||||
} @else {
|
||||
{{ selectedIcon.value }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
@@ -106,7 +115,7 @@
|
||||
label="Create Shelf"
|
||||
icon="pi pi-plus"
|
||||
severity="success"
|
||||
(onClick)="saveNewShelf()"
|
||||
(onClick)="createShelf()"
|
||||
[disabled]="!shelfName.trim()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -249,9 +249,19 @@
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
i {
|
||||
font-size: 1.25rem;
|
||||
app-icon-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary-contrast-color);
|
||||
|
||||
::ng-deep i {
|
||||
color: var(--primary-contrast-color);
|
||||
}
|
||||
|
||||
::ng-deep img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,10 +291,6 @@
|
||||
.icon-preview {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
i {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
|
||||
@@ -2,12 +2,13 @@ import {Component, inject} from '@angular/core';
|
||||
import {DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
import {MessageService} from 'primeng/api';
|
||||
import {ShelfService} from '../../service/shelf.service';
|
||||
import {IconPickerService} from '../../../../shared/service/icon-picker.service';
|
||||
import {IconPickerService, IconSelection} from '../../../../shared/service/icon-picker.service';
|
||||
import {Shelf} from '../../model/shelf.model';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Button} from 'primeng/button';
|
||||
import {InputText} from 'primeng/inputtext';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
import {IconDisplayComponent} from '../../../../shared/components/icon-display/icon-display.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shelf-creator',
|
||||
@@ -17,7 +18,8 @@ import {Tooltip} from 'primeng/tooltip';
|
||||
FormsModule,
|
||||
Button,
|
||||
InputText,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
IconDisplayComponent
|
||||
],
|
||||
styleUrl: './shelf-creator.component.scss',
|
||||
})
|
||||
@@ -28,24 +30,7 @@ export class ShelfCreatorComponent {
|
||||
private iconPickerService = inject(IconPickerService);
|
||||
|
||||
shelfName: string = '';
|
||||
selectedIcon: string | null = null;
|
||||
|
||||
saveNewShelf(): void {
|
||||
const newShelf: Partial<Shelf> = {
|
||||
name: this.shelfName,
|
||||
icon: this.selectedIcon ? this.selectedIcon.replace('pi pi-', '') : 'heart'
|
||||
};
|
||||
this.shelfService.createShelf(newShelf as Shelf).subscribe({
|
||||
next: () => {
|
||||
this.messageService.add({severity: 'info', summary: 'Success', detail: `Shelf created: ${this.shelfName}`});
|
||||
this.dynamicDialogRef.close(true);
|
||||
},
|
||||
error: (e) => {
|
||||
this.messageService.add({severity: 'error', summary: 'Error', detail: 'Failed to create shelf'});
|
||||
console.error('Error creating shelf:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
selectedIcon: IconSelection | null = null;
|
||||
|
||||
openIconPicker(): void {
|
||||
this.iconPickerService.open().subscribe(icon => {
|
||||
@@ -62,4 +47,26 @@ export class ShelfCreatorComponent {
|
||||
cancel(): void {
|
||||
this.dynamicDialogRef.close();
|
||||
}
|
||||
|
||||
createShelf(): void {
|
||||
const iconValue = this.selectedIcon?.value || 'bookmark';
|
||||
const iconType = this.selectedIcon?.type || 'PRIME_NG';
|
||||
|
||||
const newShelf: Partial<Shelf> = {
|
||||
name: this.shelfName,
|
||||
icon: iconValue,
|
||||
iconType: iconType
|
||||
};
|
||||
|
||||
this.shelfService.createShelf(newShelf as Shelf).subscribe({
|
||||
next: () => {
|
||||
this.messageService.add({severity: 'info', summary: 'Success', detail: `Shelf created: ${this.shelfName}`});
|
||||
this.dynamicDialogRef.close(true);
|
||||
},
|
||||
error: (e) => {
|
||||
this.messageService.add({severity: 'error', summary: 'Error', detail: 'Failed to create shelf'});
|
||||
console.error('Error creating shelf:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@
|
||||
@if (selectedIcon) {
|
||||
<div class="icon-container">
|
||||
<div class="icon-wrapper">
|
||||
<i [class]="selectedIcon" class="icon"></i>
|
||||
<app-icon-display
|
||||
[icon]="selectedIcon"
|
||||
size="24px"
|
||||
iconClass="icon"
|
||||
/>
|
||||
</div>
|
||||
<p-button icon="pi pi-times" (onClick)="clearSelectedIcon()" [rounded]="true" [text]="true" severity="danger"></p-button>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {ShelfService} from '../../service/shelf.service';
|
||||
import {DialogService, DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
import {Button} from 'primeng/button';
|
||||
import {InputText} from 'primeng/inputtext';
|
||||
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {Shelf} from '../../model/shelf.model';
|
||||
import {MessageService} from 'primeng/api';
|
||||
import {IconPickerService} from '../../../../shared/service/icon-picker.service';
|
||||
import {IconPickerService, IconSelection} from '../../../../shared/service/icon-picker.service';
|
||||
import {IconDisplayComponent} from '../../../../shared/components/icon-display/icon-display.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shelf-edit-dialog',
|
||||
@@ -15,7 +16,8 @@ import {IconPickerService} from '../../../../shared/service/icon-picker.service'
|
||||
Button,
|
||||
InputText,
|
||||
ReactiveFormsModule,
|
||||
FormsModule
|
||||
FormsModule,
|
||||
IconDisplayComponent
|
||||
],
|
||||
templateUrl: './shelf-edit-dialog.component.html',
|
||||
standalone: true,
|
||||
@@ -30,7 +32,7 @@ export class ShelfEditDialogComponent implements OnInit {
|
||||
private iconPickerService = inject(IconPickerService);
|
||||
|
||||
shelfName: string = '';
|
||||
selectedIcon: string | null = null;
|
||||
selectedIcon: IconSelection | null = null;
|
||||
shelf!: Shelf | undefined;
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -38,7 +40,11 @@ export class ShelfEditDialogComponent implements OnInit {
|
||||
this.shelf = this.shelfService.getShelfById(shelfId);
|
||||
if (this.shelf) {
|
||||
this.shelfName = this.shelf.name;
|
||||
this.selectedIcon = 'pi pi-' + this.shelf.icon;
|
||||
if (this.shelf.iconType === 'PRIME_NG') {
|
||||
this.selectedIcon = {type: 'PRIME_NG', value: `pi pi-${this.shelf.icon}`};
|
||||
} else {
|
||||
this.selectedIcon = {type: 'CUSTOM_SVG', value: this.shelf.icon};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,9 +61,13 @@ export class ShelfEditDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
save() {
|
||||
const iconValue = this.selectedIcon?.value || 'bookmark';
|
||||
const iconType = this.selectedIcon?.type || 'PRIME_NG';
|
||||
|
||||
const shelf: Shelf = {
|
||||
name: this.shelfName,
|
||||
icon: this.selectedIcon?.replace('pi pi-', '') || 'heart'
|
||||
icon: iconValue,
|
||||
iconType: iconType
|
||||
};
|
||||
|
||||
this.shelfService.updateShelf(shelf, this.shelf?.id).subscribe({
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface Library {
|
||||
id?: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
iconType?: 'PRIME_NG' | 'CUSTOM_SVG';
|
||||
watch: boolean;
|
||||
fileNamingPattern?: string;
|
||||
sort?: SortOption;
|
||||
|
||||
@@ -4,5 +4,6 @@ export interface Shelf {
|
||||
id?: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
iconType?: 'PRIME_NG' | 'CUSTOM_SVG';
|
||||
sort?: SortOption;
|
||||
}
|
||||
|
||||
@@ -94,11 +94,20 @@
|
||||
} @else {
|
||||
<div class="selected-icon-display">
|
||||
<div class="icon-preview">
|
||||
<i [class]="selectedIcon"></i>
|
||||
<app-icon-display
|
||||
[icon]="selectedIcon"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
<div class="icon-info">
|
||||
<span class="icon-label">Selected Icon</span>
|
||||
<span class="icon-name">{{ selectedIcon }}</span>
|
||||
<span class="icon-name">
|
||||
@if (selectedIcon.type === 'PRIME_NG') {
|
||||
{{ selectedIcon.value }}
|
||||
} @else {
|
||||
{{ selectedIcon.value }} (Custom)
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
|
||||
@@ -11,21 +11,22 @@ import {InputText} from 'primeng/inputtext';
|
||||
import {BookFileType, Library, LibraryScanMode} from '../book/model/library.model';
|
||||
import {ToggleSwitch} from 'primeng/toggleswitch';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
import {IconPickerService} from '../../shared/service/icon-picker.service';
|
||||
import {IconPickerService, IconSelection} from '../../shared/service/icon-picker.service';
|
||||
import {Select} from 'primeng/select';
|
||||
import {Button} from 'primeng/button';
|
||||
import {IconDisplayComponent} from '../../shared/components/icon-display/icon-display.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-creator',
|
||||
standalone: true,
|
||||
templateUrl: './library-creator.component.html',
|
||||
imports: [TableModule, StepPanel, FormsModule, InputText, Stepper, StepList, Step, StepPanels, ToggleSwitch, Tooltip, Select, Button],
|
||||
imports: [TableModule, StepPanel, FormsModule, InputText, Stepper, StepList, Step, StepPanels, ToggleSwitch, Tooltip, Select, Button, IconDisplayComponent],
|
||||
styleUrl: './library-creator.component.scss'
|
||||
})
|
||||
export class LibraryCreatorComponent implements OnInit {
|
||||
chosenLibraryName: string = '';
|
||||
folders: string[] = [];
|
||||
selectedIcon: string | null = null;
|
||||
selectedIcon: IconSelection | null = null;
|
||||
|
||||
mode!: string;
|
||||
library!: Library | undefined;
|
||||
@@ -61,10 +62,16 @@ export class LibraryCreatorComponent implements OnInit {
|
||||
this.mode = data.mode;
|
||||
this.library = this.libraryService.findLibraryById(data.libraryId);
|
||||
if (this.library) {
|
||||
const {name, icon, paths, watch, scanMode, defaultBookFormat} = this.library;
|
||||
const {name, icon, iconType, paths, watch, scanMode, defaultBookFormat} = this.library;
|
||||
this.chosenLibraryName = name;
|
||||
this.editModeLibraryName = name;
|
||||
this.selectedIcon = `pi pi-${icon}`;
|
||||
|
||||
if (iconType === 'CUSTOM_SVG') {
|
||||
this.selectedIcon = {type: 'CUSTOM_SVG', value: icon};
|
||||
} else {
|
||||
this.selectedIcon = {type: 'PRIME_NG', value: `pi pi-${icon}`};
|
||||
}
|
||||
|
||||
this.watch = watch;
|
||||
this.scanMode = scanMode || 'FILE_AS_BOOK';
|
||||
this.defaultBookFormat = defaultBookFormat || undefined;
|
||||
@@ -145,10 +152,14 @@ export class LibraryCreatorComponent implements OnInit {
|
||||
}
|
||||
|
||||
createOrUpdateLibrary(): void {
|
||||
const iconValue = this.selectedIcon?.value || 'heart';
|
||||
const iconType = this.selectedIcon?.type || 'PRIME_NG';
|
||||
|
||||
if (this.mode === 'edit') {
|
||||
const library: Library = {
|
||||
name: this.chosenLibraryName,
|
||||
icon: this.selectedIcon?.replace('pi pi-', '') || 'heart',
|
||||
icon: iconValue,
|
||||
iconType: iconType,
|
||||
paths: this.folders.map(folder => ({path: folder})),
|
||||
watch: this.watch,
|
||||
scanMode: this.scanMode,
|
||||
@@ -167,7 +178,8 @@ export class LibraryCreatorComponent implements OnInit {
|
||||
} else {
|
||||
const library: Library = {
|
||||
name: this.chosenLibraryName,
|
||||
icon: this.selectedIcon?.replace('pi pi-', '') || 'heart',
|
||||
icon: iconValue,
|
||||
iconType: iconType,
|
||||
paths: this.folders.map(folder => ({path: folder})),
|
||||
watch: this.watch,
|
||||
scanMode: this.scanMode,
|
||||
|
||||
@@ -29,13 +29,22 @@
|
||||
<div class="icon-picker-section">
|
||||
<div class="icon-picker-wrapper">
|
||||
<label>Shelf Icon</label>
|
||||
<p-button
|
||||
[icon]="form.get('icon')?.value || 'pi pi-plus'"
|
||||
(click)="openIconPicker()"
|
||||
[outlined]="form.get('icon')?.value"
|
||||
[severity]="form.get('icon')?.value ? 'success' : 'danger'"
|
||||
[label]="form.get('icon')?.value ? 'Icon Selected' : 'Select Icon'">
|
||||
</p-button>
|
||||
<div class="icon-display-container" (click)="openIconPicker()">
|
||||
@if (selectedIcon) {
|
||||
<app-icon-display
|
||||
[icon]="selectedIcon"
|
||||
[size]="'16px'"
|
||||
iconClass="selected-icon"
|
||||
alt="Selected shelf icon">
|
||||
</app-icon-display>
|
||||
<span class="icon-label">Click to change</span>
|
||||
} @else {
|
||||
<div class="icon-placeholder">
|
||||
<i class="pi pi-plus"></i>
|
||||
<span>Select Icon</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (isAdmin) {
|
||||
|
||||
@@ -152,6 +152,48 @@
|
||||
color: var(--text-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.icon-display-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.625rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--card-background);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.selected-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-secondary-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-secondary-color);
|
||||
|
||||
i {
|
||||
font-size: 1.125rem;
|
||||
color: var(--primary-color)
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -15,9 +15,10 @@ import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
import {MultiSelect} from 'primeng/multiselect';
|
||||
import {AutoComplete} from 'primeng/autocomplete';
|
||||
import {EMPTY_CHECK_OPERATORS, MULTI_VALUE_OPERATORS, parseValue, removeNulls, serializeDateRules} from '../service/magic-shelf-utils';
|
||||
import {IconPickerService} from '../../../shared/service/icon-picker.service';
|
||||
import {IconPickerService, IconSelection} from '../../../shared/service/icon-picker.service';
|
||||
import {CheckboxModule} from "primeng/checkbox";
|
||||
import {UserService} from "../../settings/user-management/user.service";
|
||||
import {IconDisplayComponent} from '../../../shared/components/icon-display/icon-display.component';
|
||||
|
||||
export type RuleOperator =
|
||||
| 'equals'
|
||||
@@ -154,7 +155,8 @@ const FIELD_CONFIGS: Record<RuleField, FullFieldConfig> = {
|
||||
InputNumber,
|
||||
MultiSelect,
|
||||
AutoComplete,
|
||||
CheckboxModule
|
||||
CheckboxModule,
|
||||
IconDisplayComponent
|
||||
]
|
||||
})
|
||||
export class MagicShelfComponent implements OnInit {
|
||||
@@ -210,6 +212,8 @@ export class MagicShelfComponent implements OnInit {
|
||||
userService = inject(UserService);
|
||||
private iconPicker = inject(IconPickerService);
|
||||
|
||||
selectedIcon: IconSelection | null = null;
|
||||
|
||||
trackByFn(ruleCtrl: AbstractControl, index: number): any {
|
||||
return ruleCtrl;
|
||||
}
|
||||
@@ -222,12 +226,20 @@ export class MagicShelfComponent implements OnInit {
|
||||
if (id) {
|
||||
this.shelfId = id;
|
||||
this.magicShelfService.getShelf(id).subscribe((data) => {
|
||||
const iconValue = data?.icon ?? null;
|
||||
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string | null>(data?.name ?? null, {nonNullable: true, validators: [Validators.required]}),
|
||||
icon: new FormControl<string | null>(data?.icon ?? null, {nonNullable: true, validators: [Validators.required]}),
|
||||
icon: new FormControl<string | null>(iconValue, {nonNullable: true, validators: [Validators.required]}),
|
||||
isPublic: new FormControl<boolean>(data?.isPublic ?? false),
|
||||
group: data?.filterJson ? this.buildGroupFromData(JSON.parse(data.filterJson)) : this.createGroup()
|
||||
});
|
||||
|
||||
if (iconValue) {
|
||||
this.selectedIcon = iconValue.startsWith('pi ')
|
||||
? {type: 'PRIME_NG', value: iconValue}
|
||||
: {type: 'CUSTOM_SVG', value: iconValue};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.form = new FormGroup({
|
||||
@@ -409,7 +421,11 @@ export class MagicShelfComponent implements OnInit {
|
||||
openIconPicker() {
|
||||
this.iconPicker.open().subscribe(icon => {
|
||||
if (icon) {
|
||||
this.form.get('icon')?.setValue(icon);
|
||||
this.selectedIcon = icon;
|
||||
const iconValue = icon.type === 'CUSTOM_SVG'
|
||||
? icon.value
|
||||
: icon.value;
|
||||
this.form.get('icon')?.setValue(iconValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -462,14 +478,14 @@ export class MagicShelfComponent implements OnInit {
|
||||
id: this.shelfId ?? undefined,
|
||||
name: value.name,
|
||||
icon: value.icon,
|
||||
isPublic: !!value.isPublic, // Ensure it's a boolean
|
||||
iconType: this.selectedIcon?.type,
|
||||
isPublic: !!value.isPublic,
|
||||
group: cleanedGroup
|
||||
}).subscribe({
|
||||
next: (savedShelf) => {
|
||||
this.messageService.add({severity: 'success', summary: 'Success', detail: 'Magic shelf saved successfully.'});
|
||||
if (savedShelf?.id) {
|
||||
this.shelfId = savedShelf.id;
|
||||
// Update the form with the saved data to reflect changes immediately
|
||||
this.form.patchValue({
|
||||
name: savedShelf.name,
|
||||
icon: savedShelf.icon,
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface MagicShelf {
|
||||
id?: number | null;
|
||||
name: string;
|
||||
icon?: string;
|
||||
iconType?: 'PRIME_NG' | 'CUSTOM_SVG';
|
||||
filterJson: string;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
@@ -93,11 +94,12 @@ export class MagicShelfService {
|
||||
);
|
||||
}
|
||||
|
||||
saveShelf(data: { id?: number; name: string | null; icon: string | null; group: any, isPublic?: boolean | null }): Observable<MagicShelf> {
|
||||
saveShelf(data: { id?: number; name: string | null; icon: string | null; iconType?: 'PRIME_NG' | 'CUSTOM_SVG'; group: any, isPublic?: boolean | null }): Observable<MagicShelf> {
|
||||
const payload: MagicShelf = {
|
||||
id: data.id,
|
||||
name: data.name ?? '',
|
||||
icon: data.icon ?? 'pi pi-book',
|
||||
iconType: data.iconType,
|
||||
filterJson: JSON.stringify(data.group),
|
||||
isPublic: data.isPublic ?? false
|
||||
};
|
||||
|
||||
@@ -69,7 +69,6 @@ export class CoverSearchComponent implements OnInit {
|
||||
.pipe(finalize(() => this.loading = false))
|
||||
.subscribe({
|
||||
next: (images) => {
|
||||
console.log('API response received:', images);
|
||||
this.coverImages = images.sort((a, b) => a.index - b.index);
|
||||
this.hasSearched = true;
|
||||
},
|
||||
|
||||
@@ -616,7 +616,7 @@ export class CbxReaderComponent implements OnInit {
|
||||
next: (seriesBooks) => {
|
||||
const sortedBySeriesNumber = this.sortBooksBySeriesNumber(seriesBooks);
|
||||
const currentBookIndex = sortedBySeriesNumber.findIndex(b => b.id === book.id);
|
||||
|
||||
|
||||
if (currentBookIndex === -1) {
|
||||
console.warn('[SeriesNav] Current book not found in series');
|
||||
return;
|
||||
@@ -627,14 +627,6 @@ export class CbxReaderComponent implements OnInit {
|
||||
|
||||
this.previousBookInSeries = hasPreviousBook ? sortedBySeriesNumber[currentBookIndex - 1] : null;
|
||||
this.nextBookInSeries = hasNextBook ? sortedBySeriesNumber[currentBookIndex + 1] : null;
|
||||
|
||||
console.log('[SeriesNav] Navigation loaded:', {
|
||||
series: book.metadata?.seriesName,
|
||||
totalBooks: seriesBooks.length,
|
||||
currentPosition: currentBookIndex + 1,
|
||||
hasPrevious: hasPreviousBook,
|
||||
hasNext: hasNextBook
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('[SeriesNav] Failed to load series information:', err);
|
||||
@@ -652,22 +644,22 @@ export class CbxReaderComponent implements OnInit {
|
||||
|
||||
getBookDisplayTitle(book: Book | null): string {
|
||||
if (!book) return '';
|
||||
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
|
||||
if (book.metadata?.seriesNumber) {
|
||||
parts.push(`#${book.metadata.seriesNumber}`);
|
||||
}
|
||||
|
||||
|
||||
const title = book.metadata?.title || book.fileName;
|
||||
if (title) {
|
||||
parts.push(title);
|
||||
}
|
||||
|
||||
|
||||
if (book.metadata?.subtitle) {
|
||||
parts.push(book.metadata.subtitle);
|
||||
}
|
||||
|
||||
|
||||
return parts.join(' - ');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
}
|
||||
|
||||
.directory-picker {
|
||||
width: 550px;
|
||||
max-width: 550px;
|
||||
width: 650px;
|
||||
max-width: 650px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--ground-background);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {Component, inject, Input} from '@angular/core';
|
||||
import {IconSelection} from '../../service/icon-picker.service';
|
||||
import {UrlHelperService} from '../../service/url-helper.service';
|
||||
import {NgClass, NgStyle} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-icon-display',
|
||||
standalone: true,
|
||||
imports: [NgClass, NgStyle],
|
||||
template: `
|
||||
@if (icon) {
|
||||
@if (icon.type === 'PRIME_NG') {
|
||||
<i [class]="getPrimeNgIconClass(icon.value)" [ngClass]="iconClass" [ngStyle]="iconStyle"></i>
|
||||
} @else {
|
||||
<img
|
||||
[src]="getIconUrl(icon.value)"
|
||||
[alt]="alt"
|
||||
[ngClass]="iconClass"
|
||||
[ngStyle]="getImageStyle()"
|
||||
/>
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: [`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class IconDisplayComponent {
|
||||
@Input() icon: IconSelection | null = null;
|
||||
@Input() iconClass: string = 'icon';
|
||||
@Input() iconStyle: Record<string, string> = {};
|
||||
@Input() size: string = '24px';
|
||||
@Input() alt: string = 'Icon';
|
||||
|
||||
private urlHelper = inject(UrlHelperService);
|
||||
|
||||
getPrimeNgIconClass(iconValue: string): string {
|
||||
if (iconValue.startsWith('pi pi-')) {
|
||||
return iconValue;
|
||||
}
|
||||
if (iconValue.startsWith('pi-')) {
|
||||
return `pi ${iconValue}`;
|
||||
}
|
||||
return `pi pi-${iconValue}`;
|
||||
}
|
||||
|
||||
getIconUrl(iconName: string): string {
|
||||
return this.urlHelper.getIconUrl(iconName);
|
||||
}
|
||||
|
||||
getImageStyle(): Record<string, string> {
|
||||
return {
|
||||
width: this.size,
|
||||
height: this.size,
|
||||
objectFit: 'contain',
|
||||
...this.iconStyle
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,126 @@
|
||||
<input type="text" placeholder="Search icons..." [(ngModel)]="searchText" class="icon-search"/>
|
||||
<p-tabs [(value)]="activeTabIndex">
|
||||
<p-tablist>
|
||||
<p-tab value="0">Prime Icons</p-tab>
|
||||
<p-tab value="1">SVG Icons</p-tab>
|
||||
<p-tab value="2">Add SVG Icon</p-tab>
|
||||
</p-tablist>
|
||||
<p-tabpanels>
|
||||
<p-tabpanel value="0">
|
||||
<input type="text" placeholder="Search Prime icons..." [(ngModel)]="searchText" class="icon-search"/>
|
||||
|
||||
<div class="icon-grid">
|
||||
@for (icon of filteredIcons(); track icon) {
|
||||
<div
|
||||
class="icon-item"
|
||||
(click)="selectIcon(icon)"
|
||||
[class.selected]="icon === selectedIcon">
|
||||
<i [class]="icon"></i>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="icon-grid">
|
||||
@for (icon of filteredIcons(); track icon) {
|
||||
<div
|
||||
class="icon-item"
|
||||
(click)="selectIcon(icon)"
|
||||
[class.selected]="icon === selectedIcon"
|
||||
[title]="icon">
|
||||
<i [class]="icon"></i>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</p-tabpanel>
|
||||
<p-tabpanel value="1">
|
||||
<div class="svg-browse-container">
|
||||
@if (isLoadingSvgIcons) {
|
||||
<div class="loading-message">Loading icons...</div>
|
||||
} @else if (svgIconsError) {
|
||||
<div class="error-message">{{ svgIconsError }}</div>
|
||||
} @else {
|
||||
<input type="text" placeholder="Search SVG icons..." [(ngModel)]="svgSearchText" class="icon-search"/>
|
||||
|
||||
<div class="icon-grid">
|
||||
@for (iconName of filteredSvgIcons(); track iconName) {
|
||||
<div
|
||||
class="icon-item svg-icon-item"
|
||||
(click)="selectSvgIcon(iconName)"
|
||||
[class.selected]="iconName === selectedSvgIcon"
|
||||
[title]="iconName"
|
||||
draggable="true"
|
||||
(dragstart)="onSvgIconDragStart(iconName)"
|
||||
(dragend)="onSvgIconDragEnd()"
|
||||
>
|
||||
<img
|
||||
[src]="getSvgIconUrl(iconName)"
|
||||
[alt]="iconName"
|
||||
(error)="onImageError($event)"
|
||||
class="svg-icon-image"/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (totalSvgPages > 1) {
|
||||
<div class="pagination-controls">
|
||||
<button
|
||||
(click)="loadSvgIcons(currentSvgPage - 1)"
|
||||
[disabled]="currentSvgPage === 0"
|
||||
class="pagination-button">
|
||||
Previous
|
||||
</button>
|
||||
<span class="pagination-info">
|
||||
Page {{ currentSvgPage + 1 }} of {{ totalSvgPages }}
|
||||
</span>
|
||||
<button
|
||||
(click)="loadSvgIcons(currentSvgPage + 1)"
|
||||
[disabled]="currentSvgPage >= totalSvgPages - 1"
|
||||
class="pagination-button">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style="display: flex; align-items: center; position: fixed; right: 32px; bottom: 32px; z-index: 101;">
|
||||
<div
|
||||
class="svg-trash-area"
|
||||
[class.trash-hover]="isTrashHover"
|
||||
(dragover)="onTrashDragOver($event)"
|
||||
(dragleave)="onTrashDragLeave($event)"
|
||||
(drop)="onTrashDrop($event)"
|
||||
>
|
||||
<i class="pi pi-trash"></i>
|
||||
<span>Drag here to delete icon</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</p-tabpanel>
|
||||
<p-tabpanel value="2">
|
||||
<div class="svg-paste-container">
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="svgName"
|
||||
placeholder="Enter icon name for saving"
|
||||
class="svg-name-input"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
[(ngModel)]="svgContent"
|
||||
(ngModelChange)="onSvgContentChange()"
|
||||
placeholder="Paste your SVG code here..."
|
||||
class="svg-textarea"
|
||||
rows="8"></textarea>
|
||||
|
||||
@if (svgContent && svgPreview) {
|
||||
<div class="svg-preview-section">
|
||||
<h4 class="preview-title">Preview</h4>
|
||||
<div class="svg-preview" [innerHTML]="svgPreview"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="error-message">{{ errorMessage }}</div>
|
||||
}
|
||||
|
||||
<div class="button-container">
|
||||
<p-button
|
||||
(onClick)="saveSvg()"
|
||||
[disabled]="!svgContent || !svgName"
|
||||
[loading]="isLoading"
|
||||
label="Save SVG"
|
||||
severity="primary">
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
</p-tabpanel>
|
||||
</p-tabpanels>
|
||||
</p-tabs>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(25px, 1fr));
|
||||
gap: 30px;
|
||||
gap: 38px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.icon-item > i {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.icon-search {
|
||||
@@ -18,6 +23,7 @@
|
||||
border: 1px solid var(--text-secondary-color);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -30,3 +36,192 @@
|
||||
color: var(--text-secondary-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.svg-paste-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.svg-textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--text-secondary-color);
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-preview-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.svg-preview {
|
||||
border: 1px solid var(--text-secondary-color);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background-color: var(--ground-background);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
max-height: 225px;
|
||||
overflow: auto;
|
||||
|
||||
::ng-deep svg {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #e74c3c;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 8px;
|
||||
background-color: #ffe6e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.svg-name-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 15px;
|
||||
border: 1px solid var(--text-secondary-color);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-browse-container {
|
||||
.loading-message {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
.svg-icon-image {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
|
||||
.pagination-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-trash-area {
|
||||
position: fixed;
|
||||
right: 32px;
|
||||
bottom: 32px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: var(--p-surface-800);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: 50px;
|
||||
padding: 12px 22px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.30);
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 16px;
|
||||
transition: border-color 0.2s, background 0.2s, color 0.2s;
|
||||
|
||||
i.pi-trash {
|
||||
font-size: 22px;
|
||||
color: #ff3b27;
|
||||
margin-right: 8px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #bbb;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
&.trash-hover {
|
||||
border-color: #e74c3c;
|
||||
background: #3a2323;
|
||||
color: #e74c3c;
|
||||
|
||||
i.pi-trash {
|
||||
color: #ff7675;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #ff7675;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,88 @@
|
||||
import {Component, EventEmitter, inject, Output} from '@angular/core';
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
import {IconService} from '../../services/icon.service';
|
||||
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
|
||||
import {UrlHelperService} from '../../service/url-helper.service';
|
||||
import {MessageService} from 'primeng/api';
|
||||
import {IconCategoriesHelper} from '../../helpers/icon-categories.helper';
|
||||
import {Button} from 'primeng/button';
|
||||
import {TabsModule} from 'primeng/tabs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-icon-picker-component',
|
||||
imports: [
|
||||
FormsModule
|
||||
FormsModule,
|
||||
Button,
|
||||
TabsModule
|
||||
],
|
||||
templateUrl: './icon-picker-component.html',
|
||||
styleUrl: './icon-picker-component.scss'
|
||||
})
|
||||
export class IconPickerComponent {
|
||||
export class IconPickerComponent implements OnInit {
|
||||
|
||||
iconCategories: string[] = [
|
||||
"address-book", "align-center", "align-justify", "align-left", "align-right", "amazon", "android",
|
||||
"angle-double-down", "angle-double-left", "angle-double-right", "angle-double-up", "angle-down", "angle-left",
|
||||
"angle-right", "angle-up", "apple", "arrow-circle-down", "arrow-circle-left", "arrow-circle-right", "arrow-circle-up",
|
||||
"arrow-down", "arrow-down-left", "arrow-down-left-and-arrow-up-right-to-center", "arrow-down-right", "arrow-left",
|
||||
"arrow-right", "arrow-right-arrow-left", "arrow-up", "arrow-up-left", "arrow-up-right", "arrow-up-right-and-arrow-down-left-from-center",
|
||||
"arrows-alt", "arrows-h", "arrows-v", "asterisk", "at", "backward", "ban", "barcode", "bars", "bell", "bell-slash",
|
||||
"bitcoin", "bolt", "book", "bookmark", "bookmark-fill", "box", "briefcase", "building", "building-columns", "bullseye",
|
||||
"calculator", "calendar", "calendar-clock", "calendar-minus", "calendar-plus", "calendar-times", "camera", "car",
|
||||
"caret-down", "caret-left", "caret-right", "caret-up", "cart-arrow-down", "cart-minus", "cart-plus", "chart-bar",
|
||||
"chart-line", "chart-pie", "chart-scatter", "check", "check-circle", "check-square", "chevron-circle-down",
|
||||
"chevron-circle-left", "chevron-circle-right", "chevron-circle-up", "chevron-down", "chevron-left", "chevron-right",
|
||||
"chevron-up", "circle", "circle-fill", "circle-off", "circle-on", "clipboard", "clock", "clone", "cloud", "cloud-download",
|
||||
"cloud-upload", "code", "cog", "comment", "comments", "compass", "copy", "credit-card", "crown", "database", "delete-left",
|
||||
"desktop", "directions", "directions-alt", "discord", "dollar", "download", "eject", "ellipsis-h", "ellipsis-v",
|
||||
"envelope", "equals", "eraser", "ethereum", "euro", "exclamation-circle", "exclamation-triangle", "expand",
|
||||
"external-link", "eye", "eye-slash", "face-smile", "facebook", "fast-backward", "fast-forward", "file", "file-arrow-up",
|
||||
"file-check", "file-edit", "file-excel", "file-export", "file-import", "file-o", "file-pdf", "file-plus", "file-word",
|
||||
"filter", "filter-fill", "filter-slash", "flag", "flag-fill", "folder", "folder-open", "folder-plus", "forward", "gauge",
|
||||
"gift", "github", "globe", "google", "graduation-cap", "hammer", "hashtag", "headphones", "heart", "heart-fill", "history",
|
||||
"home", "hourglass", "id-card", "image", "images", "inbox", "indian-rupee", "info", "info-circle", "instagram", "key",
|
||||
"language", "lightbulb", "link", "linkedin", "list", "list-check", "lock", "lock-open", "map", "map-marker", "mars", "megaphone",
|
||||
"microchip", "microchip-ai", "microphone", "microsoft", "minus", "minus-circle", "mobile", "money-bill", "moon", "objects-column",
|
||||
"palette", "paperclip", "pause", "pause-circle", "paypal", "pen-to-square", "pencil", "percentage", "phone", "pinterest", "play",
|
||||
"play-circle", "plus", "plus-circle", "pound", "power-off", "prime", "print", "qrcode", "question", "question-circle", "receipt",
|
||||
"reddit", "refresh", "replay", "reply", "save", "search", "search-minus", "search-plus", "send", "server", "share-alt", "shield",
|
||||
"shop", "shopping-bag", "shopping-cart", "sign-in", "sign-out", "sitemap", "slack", "sliders-h", "sliders-v", "sort",
|
||||
"sort-alpha-down", "sort-alpha-down-alt", "sort-alpha-up", "sort-alpha-up-alt", "sort-alt", "sort-alt-slash",
|
||||
"sort-amount-down", "sort-amount-down-alt", "sort-amount-up", "sort-amount-up-alt", "sort-down", "sort-down-fill",
|
||||
"sort-numeric-down", "sort-numeric-down-alt", "sort-numeric-up", "sort-numeric-up-alt", "sort-up", "sort-up-fill", "sparkles",
|
||||
"spinner", "spinner-dotted", "star", "star-fill", "star-half", "star-half-fill", "step-backward", "step-backward-alt",
|
||||
"step-forward", "step-forward-alt", "stop", "stop-circle", "stopwatch", "sun", "sync", "table", "tablet", "tag", "tags",
|
||||
"telegram", "th-large", "thumbs-down", "thumbs-down-fill", "thumbs-up", "thumbs-up-fill", "thumbtack", "ticket", "tiktok",
|
||||
"times", "times-circle", "trash", "trophy", "truck", "turkish-lira", "twitch", "twitter", "undo", "unlock", "upload", "user",
|
||||
"user-edit", "user-minus", "user-plus", "users", "venus", "verified", "video", "vimeo", "volume-down", "volume-off",
|
||||
"volume-up", "wallet", "warehouse", "wave-pulse", "whatsapp", "wifi", "window-maximize", "window-minimize", "wrench",
|
||||
"youtube"
|
||||
];
|
||||
private readonly SVG_PAGE_SIZE = 50;
|
||||
private readonly MAX_ICON_NAME_LENGTH = 255;
|
||||
private readonly MAX_SVG_SIZE = 1048576; // 1MB
|
||||
private readonly ICON_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
||||
private readonly ERROR_MESSAGES = {
|
||||
NO_CONTENT: 'Please paste SVG content',
|
||||
NO_NAME: 'Please provide a name for the icon',
|
||||
INVALID_NAME: 'Icon name can only contain alphanumeric characters and hyphens',
|
||||
NAME_TOO_LONG: `Icon name must not exceed ${this.MAX_ICON_NAME_LENGTH} characters`,
|
||||
INVALID_SVG: 'Invalid SVG content. Please paste valid SVG code.',
|
||||
MISSING_SVG_TAG: 'Content must include <svg> tag',
|
||||
SVG_TOO_LARGE: 'SVG content must not exceed 1MB',
|
||||
PARSE_ERROR: 'Failed to parse SVG content',
|
||||
LOAD_ICONS_ERROR: 'Failed to load SVG icons. Please try again.',
|
||||
SAVE_ERROR: 'Failed to save SVG. Please try again.',
|
||||
DELETE_ERROR: 'Failed to delete icon. Please try again.'
|
||||
};
|
||||
|
||||
ref = inject(DynamicDialogRef);
|
||||
iconService = inject(IconService);
|
||||
sanitizer = inject(DomSanitizer);
|
||||
urlHelper = inject(UrlHelperService);
|
||||
messageService = inject(MessageService);
|
||||
|
||||
searchText: string = '';
|
||||
selectedIcon: string | null = null;
|
||||
icons: string[] = this.createIconList(this.iconCategories);
|
||||
icons: string[] = IconCategoriesHelper.createIconList();
|
||||
|
||||
ref = inject(DynamicDialogRef);
|
||||
private _activeTabIndex: string = '0';
|
||||
|
||||
createIconList(categories: string[]): string[] {
|
||||
return categories.map(iconName => `pi pi-${iconName}`);
|
||||
get activeTabIndex(): string {
|
||||
return this._activeTabIndex;
|
||||
}
|
||||
|
||||
set activeTabIndex(value: string) {
|
||||
this._activeTabIndex = value;
|
||||
if (value === '1' && this.svgIcons.length === 0 && !this.isLoadingSvgIcons) {
|
||||
this.loadSvgIcons(0);
|
||||
}
|
||||
}
|
||||
|
||||
svgContent: string = '';
|
||||
svgName: string = '';
|
||||
svgPreview: SafeHtml | null = null;
|
||||
isLoading: boolean = false;
|
||||
errorMessage: string = '';
|
||||
|
||||
svgIcons: string[] = [];
|
||||
svgSearchText: string = '';
|
||||
currentSvgPage: number = 0;
|
||||
totalSvgPages: number = 0;
|
||||
isLoadingSvgIcons: boolean = false;
|
||||
svgIconsError: string = '';
|
||||
selectedSvgIcon: string | null = null;
|
||||
|
||||
draggedSvgIcon: string | null = null;
|
||||
isTrashHover: boolean = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.activeTabIndex === '1') {
|
||||
this.loadSvgIcons(0);
|
||||
}
|
||||
}
|
||||
|
||||
filteredIcons(): string[] {
|
||||
@@ -66,12 +90,192 @@ export class IconPickerComponent {
|
||||
return this.icons.filter(icon => icon.toLowerCase().includes(this.searchText.toLowerCase()));
|
||||
}
|
||||
|
||||
selectIcon(icon: string) {
|
||||
this.selectedIcon = icon;
|
||||
this.ref.close(icon);
|
||||
filteredSvgIcons(): string[] {
|
||||
if (!this.svgSearchText) return this.svgIcons;
|
||||
return this.svgIcons.filter(icon => icon.toLowerCase().includes(this.svgSearchText.toLowerCase()));
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.ref.close();
|
||||
selectIcon(icon: string): void {
|
||||
this.selectedIcon = icon;
|
||||
this.ref.close({type: 'PRIME_NG', value: icon});
|
||||
}
|
||||
|
||||
loadSvgIcons(page: number): void {
|
||||
this.isLoadingSvgIcons = true;
|
||||
this.svgIconsError = '';
|
||||
|
||||
this.iconService.getIconNames(page, this.SVG_PAGE_SIZE).subscribe({
|
||||
next: (response) => {
|
||||
this.svgIcons = response.content;
|
||||
this.currentSvgPage = response.number;
|
||||
this.totalSvgPages = response.totalPages;
|
||||
this.isLoadingSvgIcons = false;
|
||||
},
|
||||
error: () => {
|
||||
this.isLoadingSvgIcons = false;
|
||||
this.svgIconsError = this.ERROR_MESSAGES.LOAD_ICONS_ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSvgIconUrl(iconName: string): string {
|
||||
return this.urlHelper.getIconUrl(iconName);
|
||||
}
|
||||
|
||||
onImageError(event: Event): void {
|
||||
const img = event.target as HTMLImageElement;
|
||||
img.src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"%3E%3Ccircle cx="12" cy="12" r="10"/%3E%3Cline x1="15" y1="9" x2="9" y2="15"/%3E%3Cline x1="9" y1="9" x2="15" y2="15"/%3E%3C/svg%3E';
|
||||
}
|
||||
|
||||
selectSvgIcon(iconName: string): void {
|
||||
this.selectedSvgIcon = iconName;
|
||||
this.ref.close({type: 'CUSTOM_SVG', value: iconName});
|
||||
}
|
||||
|
||||
onSvgContentChange(): void {
|
||||
this.errorMessage = '';
|
||||
|
||||
if (!this.svgContent.trim()) {
|
||||
this.svgPreview = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedContent = this.svgContent.trim();
|
||||
if (!trimmedContent.includes('<svg')) {
|
||||
this.svgPreview = null;
|
||||
this.errorMessage = this.ERROR_MESSAGES.MISSING_SVG_TAG;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.svgPreview = this.sanitizer.bypassSecurityTrustHtml(this.svgContent);
|
||||
} catch {
|
||||
this.svgPreview = null;
|
||||
this.errorMessage = this.ERROR_MESSAGES.PARSE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
saveSvg(): void {
|
||||
const validationError = this.validateSvgInput();
|
||||
if (validationError) {
|
||||
this.errorMessage = validationError;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
this.iconService.saveSvgIcon(this.svgContent, this.svgName).subscribe({
|
||||
next: () => {
|
||||
this.isLoading = false;
|
||||
this.handleSuccessfulSave();
|
||||
},
|
||||
error: (error) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage = error.error?.details?.join(', ')
|
||||
|| error.error?.message
|
||||
|| this.ERROR_MESSAGES.SAVE_ERROR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private validateSvgInput(): string | null {
|
||||
if (!this.svgContent.trim()) {
|
||||
return this.ERROR_MESSAGES.NO_CONTENT;
|
||||
}
|
||||
|
||||
if (!this.svgName.trim()) {
|
||||
return this.ERROR_MESSAGES.NO_NAME;
|
||||
}
|
||||
|
||||
if (!this.ICON_NAME_PATTERN.test(this.svgName)) {
|
||||
return this.ERROR_MESSAGES.INVALID_NAME;
|
||||
}
|
||||
|
||||
if (this.svgName.length > this.MAX_ICON_NAME_LENGTH) {
|
||||
return this.ERROR_MESSAGES.NAME_TOO_LONG;
|
||||
}
|
||||
|
||||
if (!this.svgContent.trim().includes('<svg')) {
|
||||
return this.ERROR_MESSAGES.INVALID_SVG;
|
||||
}
|
||||
|
||||
if (this.svgContent.length > this.MAX_SVG_SIZE) {
|
||||
return this.ERROR_MESSAGES.SVG_TOO_LARGE;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private handleSuccessfulSave(): void {
|
||||
this.activeTabIndex = '1';
|
||||
if (!this.svgIcons.includes(this.svgName)) {
|
||||
this.svgIcons.unshift(this.svgName);
|
||||
}
|
||||
this.selectedSvgIcon = this.svgName;
|
||||
this.resetSvgForm();
|
||||
}
|
||||
|
||||
private resetSvgForm(): void {
|
||||
this.svgSearchText = '';
|
||||
this.svgContent = '';
|
||||
this.svgName = '';
|
||||
this.svgPreview = null;
|
||||
}
|
||||
|
||||
onSvgIconDragStart(iconName: string): void {
|
||||
this.draggedSvgIcon = iconName;
|
||||
}
|
||||
|
||||
onSvgIconDragEnd(): void {
|
||||
this.draggedSvgIcon = null;
|
||||
this.isTrashHover = false;
|
||||
}
|
||||
|
||||
onTrashDragOver(event: DragEvent): void {
|
||||
event.preventDefault();
|
||||
this.isTrashHover = true;
|
||||
}
|
||||
|
||||
onTrashDragLeave(event: DragEvent): void {
|
||||
event.preventDefault();
|
||||
this.isTrashHover = false;
|
||||
}
|
||||
|
||||
onTrashDrop(event: DragEvent): void {
|
||||
event.preventDefault();
|
||||
this.isTrashHover = false;
|
||||
|
||||
if (!this.draggedSvgIcon) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.deleteSvgIcon(this.draggedSvgIcon);
|
||||
this.draggedSvgIcon = null;
|
||||
}
|
||||
|
||||
private deleteSvgIcon(iconName: string): void {
|
||||
this.isLoadingSvgIcons = true;
|
||||
|
||||
this.iconService.deleteSvgIcon(iconName).subscribe({
|
||||
next: () => {
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Icon Deleted',
|
||||
detail: 'SVG icon deleted successfully.',
|
||||
life: 2500
|
||||
});
|
||||
this.loadSvgIcons(this.currentSvgPage);
|
||||
},
|
||||
error: (error) => {
|
||||
this.isLoadingSvgIcons = false;
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Delete Failed',
|
||||
detail: error.error?.message || this.ERROR_MESSAGES.DELETE_ERROR,
|
||||
life: 4000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
44
booklore-ui/src/app/shared/helpers/icon-categories.helper.ts
Normal file
44
booklore-ui/src/app/shared/helpers/icon-categories.helper.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export class IconCategoriesHelper {
|
||||
static readonly CATEGORIES: string[] = [
|
||||
"address-book", "align-center", "align-justify", "align-left", "align-right", "amazon", "android",
|
||||
"angle-double-down", "angle-double-left", "angle-double-right", "angle-double-up", "angle-down", "angle-left",
|
||||
"angle-right", "angle-up", "apple", "arrow-circle-down", "arrow-circle-left", "arrow-circle-right", "arrow-circle-up",
|
||||
"arrow-down", "arrow-down-left", "arrow-down-left-and-arrow-up-right-to-center", "arrow-down-right", "arrow-left",
|
||||
"arrow-right", "arrow-right-arrow-left", "arrow-up", "arrow-up-left", "arrow-up-right", "arrow-up-right-and-arrow-down-left-from-center",
|
||||
"arrows-alt", "arrows-h", "arrows-v", "asterisk", "at", "backward", "ban", "barcode", "bars", "bell", "bell-slash",
|
||||
"bitcoin", "bolt", "book", "bookmark", "bookmark-fill", "box", "briefcase", "building", "building-columns", "bullseye",
|
||||
"calculator", "calendar", "calendar-clock", "calendar-minus", "calendar-plus", "calendar-times", "camera", "car",
|
||||
"caret-down", "caret-left", "caret-right", "caret-up", "cart-arrow-down", "cart-minus", "cart-plus", "chart-bar",
|
||||
"chart-line", "chart-pie", "chart-scatter", "check", "check-circle", "check-square", "chevron-circle-down",
|
||||
"chevron-circle-left", "chevron-circle-right", "chevron-circle-up", "chevron-down", "chevron-left", "chevron-right",
|
||||
"chevron-up", "circle", "circle-fill", "circle-off", "circle-on", "clipboard", "clock", "clone", "cloud", "cloud-download",
|
||||
"cloud-upload", "code", "cog", "comment", "comments", "compass", "copy", "credit-card", "crown", "database", "delete-left",
|
||||
"desktop", "directions", "directions-alt", "discord", "dollar", "download", "eject", "ellipsis-h", "ellipsis-v",
|
||||
"envelope", "equals", "eraser", "ethereum", "euro", "exclamation-circle", "exclamation-triangle", "expand",
|
||||
"external-link", "eye", "eye-slash", "face-smile", "facebook", "fast-backward", "fast-forward", "file", "file-arrow-up",
|
||||
"file-check", "file-edit", "file-excel", "file-export", "file-import", "file-o", "file-pdf", "file-plus", "file-word",
|
||||
"filter", "filter-fill", "filter-slash", "flag", "flag-fill", "folder", "folder-open", "folder-plus", "forward", "gauge",
|
||||
"gift", "github", "globe", "google", "graduation-cap", "hammer", "hashtag", "headphones", "heart", "heart-fill", "history",
|
||||
"home", "hourglass", "id-card", "image", "images", "inbox", "indian-rupee", "info", "info-circle", "instagram", "key",
|
||||
"language", "lightbulb", "link", "linkedin", "list", "list-check", "lock", "lock-open", "map", "map-marker", "mars", "megaphone",
|
||||
"microchip", "microchip-ai", "microphone", "microsoft", "minus", "minus-circle", "mobile", "money-bill", "moon", "objects-column",
|
||||
"palette", "paperclip", "pause", "pause-circle", "paypal", "pen-to-square", "pencil", "percentage", "phone", "pinterest", "play",
|
||||
"play-circle", "plus", "plus-circle", "pound", "power-off", "prime", "print", "qrcode", "question", "question-circle", "receipt",
|
||||
"reddit", "refresh", "replay", "reply", "save", "search", "search-minus", "search-plus", "send", "server", "share-alt", "shield",
|
||||
"shop", "shopping-bag", "shopping-cart", "sign-in", "sign-out", "sitemap", "slack", "sliders-h", "sliders-v", "sort",
|
||||
"sort-alpha-down", "sort-alpha-down-alt", "sort-alpha-up", "sort-alpha-up-alt", "sort-alt", "sort-alt-slash",
|
||||
"sort-amount-down", "sort-amount-down-alt", "sort-amount-up", "sort-amount-up-alt", "sort-down", "sort-down-fill",
|
||||
"sort-numeric-down", "sort-numeric-down-alt", "sort-numeric-up", "sort-numeric-up-alt", "sort-up", "sort-up-fill", "sparkles",
|
||||
"spinner", "spinner-dotted", "star", "star-fill", "star-half", "star-half-fill", "step-backward", "step-backward-alt",
|
||||
"step-forward", "step-forward-alt", "stop", "stop-circle", "stopwatch", "sun", "sync", "table", "tablet", "tag", "tags",
|
||||
"telegram", "th-large", "thumbs-down", "thumbs-down-fill", "thumbs-up", "thumbs-up-fill", "thumbtack", "ticket", "tiktok",
|
||||
"times", "times-circle", "trash", "trophy", "truck", "turkish-lira", "twitch", "twitter", "undo", "unlock", "upload", "user",
|
||||
"user-edit", "user-minus", "user-plus", "users", "venus", "verified", "video", "vimeo", "volume-down", "volume-off",
|
||||
"volume-up", "wallet", "warehouse", "wave-pulse", "whatsapp", "wifi", "window-maximize", "window-minimize", "wrench",
|
||||
"youtube"
|
||||
];
|
||||
|
||||
static createIconList(): string[] {
|
||||
return this.CATEGORIES.map(iconName => `pi pi-${iconName}`);
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,8 @@ export class AppMenuComponent implements OnInit {
|
||||
menu: this.libraryShelfMenuService.initializeLibraryMenuItems(library),
|
||||
label: library.name,
|
||||
type: 'Library',
|
||||
icon: 'pi pi-' + library.icon,
|
||||
icon: library.icon,
|
||||
iconType: (library.iconType || 'PRIME_NG') as 'PRIME_NG' | 'CUSTOM_SVG',
|
||||
routerLink: [`/library/${library.id}/books`],
|
||||
bookCount$: this.libraryService.getBookCount(library.id ?? 0),
|
||||
})),
|
||||
@@ -129,7 +130,8 @@ export class AppMenuComponent implements OnInit {
|
||||
items: sortedShelves.map((shelf) => ({
|
||||
label: shelf.name,
|
||||
type: 'magicShelfItem',
|
||||
icon: 'pi pi-' + shelf.icon,
|
||||
icon: shelf.icon || 'pi pi-book',
|
||||
iconType: (shelf.iconType || 'PRIME_NG') as 'PRIME_NG' | 'CUSTOM_SVG',
|
||||
menu: this.libraryShelfMenuService.initializeMagicShelfMenuItems(shelf),
|
||||
routerLink: [`/magic-shelf/${shelf.id}/books`],
|
||||
bookCount$: this.magicShelfService.getBookCount(shelf.id ?? 0),
|
||||
@@ -154,7 +156,8 @@ export class AppMenuComponent implements OnInit {
|
||||
menu: this.libraryShelfMenuService.initializeShelfMenuItems(shelf),
|
||||
label: shelf.name,
|
||||
type: 'Shelf',
|
||||
icon: 'pi pi-' + shelf.icon,
|
||||
icon: shelf.icon,
|
||||
iconType: (shelf.iconType || 'PRIME_NG') as 'PRIME_NG' | 'CUSTOM_SVG',
|
||||
routerLink: [`/shelf/${shelf.id}/books`],
|
||||
bookCount$: this.shelfService.getBookCount(shelf.id ?? 0),
|
||||
}));
|
||||
@@ -163,6 +166,7 @@ export class AppMenuComponent implements OnInit {
|
||||
label: 'Unshelved',
|
||||
type: 'Shelf',
|
||||
icon: 'pi pi-inbox',
|
||||
iconType: 'PRIME_NG' as 'PRIME_NG' | 'CUSTOM_SVG',
|
||||
routerLink: ['/unshelved-books'],
|
||||
bookCount$: this.shelfService.getUnshelvedBookCount?.() ?? of(0),
|
||||
};
|
||||
@@ -172,7 +176,8 @@ export class AppMenuComponent implements OnInit {
|
||||
items.push({
|
||||
label: koboShelf.name,
|
||||
type: 'Shelf',
|
||||
icon: 'pi pi-' + koboShelf.icon,
|
||||
icon: koboShelf.icon,
|
||||
iconType: (koboShelf.iconType || 'PRIME_NG') as 'PRIME_NG' | 'CUSTOM_SVG',
|
||||
routerLink: [`/shelf/${koboShelf.id}/books`],
|
||||
bookCount$: this.shelfService.getBookCount(koboShelf.id ?? 0),
|
||||
});
|
||||
|
||||
@@ -53,7 +53,11 @@
|
||||
tabindex="0"
|
||||
pRipple
|
||||
>
|
||||
<i [ngClass]="item.icon" class="layout-menuitem-icon"></i>
|
||||
<app-icon-display
|
||||
[icon]="getIconSelection()"
|
||||
iconClass="layout-menuitem-icon"
|
||||
size="19px"
|
||||
></app-icon-display>
|
||||
<span class="layout-menuitem-text menu-item-text">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.menu-item-end-content {
|
||||
|
||||
@@ -10,8 +10,9 @@ import {Button} from 'primeng/button';
|
||||
import {Menu} from 'primeng/menu';
|
||||
import {UserService} from '../../../../features/settings/user-management/user.service';
|
||||
import {DialogLauncherService} from '../../../services/dialog-launcher.service';
|
||||
import {ShelfCreatorComponent} from '../../../../features/book/components/shelf-creator/shelf-creator.component';
|
||||
import {BookDialogHelperService} from '../../../../features/book/components/book-browser/BookDialogHelperService';
|
||||
import {IconDisplayComponent} from '../../../components/icon-display/icon-display.component';
|
||||
import {IconSelection} from '../../../service/icon-picker.service';
|
||||
|
||||
@Component({
|
||||
selector: '[app-menuitem]',
|
||||
@@ -24,7 +25,8 @@ import {BookDialogHelperService} from '../../../../features/book/components/book
|
||||
Ripple,
|
||||
AsyncPipe,
|
||||
Button,
|
||||
Menu
|
||||
Menu,
|
||||
IconDisplayComponent
|
||||
],
|
||||
animations: [
|
||||
trigger('children', [
|
||||
@@ -169,6 +171,15 @@ export class AppMenuitemComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
getIconSelection(): IconSelection | null {
|
||||
if (!this.item.icon) return null;
|
||||
|
||||
return {
|
||||
type: this.item.iconType || 'PRIME_NG',
|
||||
value: this.item.icon
|
||||
};
|
||||
}
|
||||
|
||||
@HostBinding('class.active-menuitem')
|
||||
get activeClass() {
|
||||
return this.active && !this.root;
|
||||
|
||||
@@ -23,10 +23,4 @@ export class BackgroundUploadService {
|
||||
map(resp => resp?.url)
|
||||
);
|
||||
}
|
||||
|
||||
resetToDefault(): Observable<void> {
|
||||
return this.http.delete<void>(this.baseUrl).pipe(
|
||||
tap(() => console.log('Background reset to default'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@ import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
import {IconPickerComponent} from '../components/icon-picker/icon-picker-component';
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
export interface IconSelection {
|
||||
type: 'PRIME_NG' | 'CUSTOM_SVG';
|
||||
value: string;
|
||||
}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class IconPickerService {
|
||||
private dialog = inject(DialogService);
|
||||
|
||||
open(): Observable<string> {
|
||||
open(): Observable<IconSelection> {
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
const ref: DynamicDialogRef | null = this.dialog.open(IconPickerComponent, {
|
||||
header: 'Choose an Icon',
|
||||
@@ -22,6 +27,6 @@ export class IconPickerService {
|
||||
minWidth: isMobile ? '90vw' : '800px',
|
||||
}
|
||||
});
|
||||
return ref!.onClose as Observable<string>;
|
||||
return ref!.onClose as Observable<IconSelection>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,4 +100,9 @@ export class UrlHelperService {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getIconUrl(iconName: string): string {
|
||||
const url = `${this.mediaBaseUrl}/icon/${iconName}`;
|
||||
return this.appendToken(url);
|
||||
}
|
||||
}
|
||||
|
||||
39
booklore-ui/src/app/shared/services/icon.service.ts
Normal file
39
booklore-ui/src/app/shared/services/icon.service.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Observable} from 'rxjs';
|
||||
import {API_CONFIG} from '../../core/config/api-config';
|
||||
|
||||
interface PageResponse<T> {
|
||||
content: T[];
|
||||
number: number;
|
||||
size: number;
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class IconService {
|
||||
|
||||
private readonly baseUrl = `${API_CONFIG.BASE_URL}/api/v1/icons`;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
|
||||
saveSvgIcon(svgContent: string, svgName: string): Observable<any> {
|
||||
return this.http.post(this.baseUrl, {
|
||||
svgData: svgContent,
|
||||
svgName: svgName
|
||||
});
|
||||
}
|
||||
|
||||
getIconNames(page: number = 0, size: number = 50): Observable<PageResponse<string>> {
|
||||
return this.http.get<PageResponse<string>>(this.baseUrl, {
|
||||
params: { page: page.toString(), size: size.toString() }
|
||||
});
|
||||
}
|
||||
|
||||
deleteSvgIcon(svgName: string): Observable<any> {
|
||||
return this.http.delete(`${this.baseUrl}/${encodeURIComponent(svgName)}`);
|
||||
}
|
||||
}
|
||||
@@ -78,10 +78,6 @@
|
||||
border-radius: variables.$borderRadius;
|
||||
transition: background-color variables.$transitionDuration, box-shadow variables.$transitionDuration;
|
||||
|
||||
.layout-menuitem-icon {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.layout-submenu-toggler {
|
||||
font-size: 75%;
|
||||
margin-left: auto;
|
||||
|
||||
Reference in New Issue
Block a user