mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
Improve UI compatibility for mobile devices
This commit is contained in:
committed by
Aditya Chandel
parent
c8b994ed82
commit
d0edf800c7
@@ -1,9 +1,10 @@
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-col flex-grow">
|
||||
<div class="flex justify-between items-center p-2 rounded-t-xl mb-4 bg-[var(--card-background)]">
|
||||
<div class="flex items-center pl-2">
|
||||
<div class="relative flex flex-row">
|
||||
<div class="flex flex-col w-full">
|
||||
|
||||
<div class="flex items-center justify-between w-full gap-4 py-2 px-2 md:px-4 rounded-t-xl mb-4 bg-[var(--card-background)]">
|
||||
<div class="flex items-center min-w-0 max-w-full flex-shrink">
|
||||
@if (entityType$ | async; as entityType) {
|
||||
<p class="text-2xl whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<p class="text-xl whitespace-nowrap overflow-hidden text-ellipsis w-full">
|
||||
@if (entityType === EntityType.ALL_BOOKS) {
|
||||
@if (currentFilterLabel && rawFilterParamFromUrl) {
|
||||
{{ currentFilterLabel }}
|
||||
@@ -22,17 +23,22 @@
|
||||
@if (userService.userState$ | async; as userData) {
|
||||
@if (entityType !== EntityType.ALL_BOOKS && entityType !== EntityType.UNSHELVED &&
|
||||
(userData.permissions.admin || userData.permissions.canManipulateLibrary)) {
|
||||
<div>
|
||||
<p-button icon="pi pi-ellipsis-v" [rounded]="true" [text]="true" severity="info" (click)="entitymenu.toggle($event)"></p-button>
|
||||
<div class="ml-2 flex-shrink-0">
|
||||
<p-button
|
||||
icon="pi pi-ellipsis-v"
|
||||
[rounded]="true"
|
||||
[text]="true"
|
||||
severity="info"
|
||||
(click)="entitymenu.toggle($event)">
|
||||
</p-button>
|
||||
<p-menu #entitymenu [model]="entityOptions" [popup]="true" appendTo="body"></p-menu>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="flex flex-row items-center gap-6">
|
||||
<div class="flex flex-wrap items-center gap-4 flex-shrink-0">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
@if (isFilterActive) {
|
||||
<a class="topbar-items topbar-item" (click)="clearFilter()">
|
||||
<i
|
||||
@@ -119,29 +125,62 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p-fluid>
|
||||
<div class="relative">
|
||||
<i class="pi pi-search search-icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
pInputText
|
||||
placeholder="Title, Series or Author..."
|
||||
[(ngModel)]="bookTitle"
|
||||
(ngModelChange)="onSearchTermChange($event)"
|
||||
class="search-input"
|
||||
/>
|
||||
@if (bookTitle) {
|
||||
<!-- MOBILE SEARCH (only visible below md) -->
|
||||
<div class="relative md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="topbar-items topbar-item"
|
||||
(click)="searchDropdown.toggle($event)"
|
||||
pTooltip="Search"
|
||||
tooltipPosition="top"
|
||||
>
|
||||
<i class="pi pi-search"></i>
|
||||
</button>
|
||||
|
||||
<p-overlayPanel #searchDropdown [dismissable]="true" appendTo="body" [style]="{ width: '18rem' }">
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
type="text"
|
||||
pInputText
|
||||
placeholder="Title, Series or Author..."
|
||||
[(ngModel)]="bookTitle"
|
||||
(ngModelChange)="onSearchTermChange($event)"
|
||||
class="w-full"
|
||||
/>
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
[text]="true"
|
||||
size="small"
|
||||
[rounded]="true"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2"
|
||||
(click)="clearSearch()">
|
||||
</p-button>
|
||||
}
|
||||
</div>
|
||||
</p-fluid>
|
||||
(click)="clearSearch(); searchDropdown.hide()"
|
||||
></p-button>
|
||||
</div>
|
||||
</p-overlayPanel>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:block relative">
|
||||
<i class="pi pi-search search-icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
pInputText
|
||||
placeholder="Title, Series or Author..."
|
||||
[(ngModel)]="bookTitle"
|
||||
(ngModelChange)="onSearchTermChange($event)"
|
||||
class="search-input"
|
||||
/>
|
||||
@if (bookTitle) {
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
[text]="true"
|
||||
size="small"
|
||||
[rounded]="true"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2"
|
||||
(click)="clearSearch()"
|
||||
></p-button>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
<p-button
|
||||
class="pr-2"
|
||||
@@ -312,5 +351,19 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<app-book-filter [entity$]="entity$" [entityType$]="entityType$" [resetFilter$]="resetFilterSubject"></app-book-filter>
|
||||
@if (this.showFilter) {
|
||||
<div
|
||||
class="mobile-filter-mask"
|
||||
(click)="this.showFilter = false">
|
||||
</div>
|
||||
}
|
||||
|
||||
<app-book-filter
|
||||
[showFilter]="showFilter"
|
||||
class="filter-overlay-container z-50 flex-shrink-0"
|
||||
[ngClass]="{ 'active': showFilter, 'ml-4': showFilter }"
|
||||
[entity$]="entity$"
|
||||
[entityType$]="entityType$"
|
||||
[resetFilter$]="resetFilterSubject">
|
||||
</app-book-filter>
|
||||
</div>
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(1px, 1fr));
|
||||
gap: 1.3rem;
|
||||
overflow: hidden;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.virtual-scroller-item {
|
||||
@@ -44,19 +44,6 @@
|
||||
border-width: 1px 1px 0px 1px;
|
||||
}
|
||||
|
||||
.topbar-items {
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 0.3rem;
|
||||
align-items: center;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -102,3 +89,54 @@
|
||||
line-height: 2.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.mobile-filter-mask {
|
||||
@media (max-width: 991px) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 998;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-overlay-container {
|
||||
|
||||
@media (max-width: 991px) {
|
||||
width: 225px;
|
||||
position: fixed;
|
||||
top: 3.85rem;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: calc(100% - 3.85rem);
|
||||
background-color: var(--card-background);
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.15);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1001;
|
||||
|
||||
&.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:not(.active) {
|
||||
width: 0 !important;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
width: 225px;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import {InputText} from 'primeng/inputtext';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {BookFilterComponent} from './book-filter/book-filter.component';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
import {Fluid} from 'primeng/fluid';
|
||||
import {EntityViewPreferences, UserService} from '../../../settings/user-management/user.service';
|
||||
import {OverlayPanelModule} from 'primeng/overlaypanel';
|
||||
import {SeriesCollapseFilter} from './filters/SeriesCollapseFilter';
|
||||
@@ -74,7 +73,7 @@ const SORT_DIRECTION = {
|
||||
standalone: true,
|
||||
templateUrl: './book-browser.component.html',
|
||||
styleUrls: ['./book-browser.component.scss'],
|
||||
imports: [Button, VirtualScrollerModule, BookCardComponent, AsyncPipe, ProgressSpinner, Menu, InputText, FormsModule, BookTableComponent, BookFilterComponent, Tooltip, NgClass, Fluid, PrimeTemplate, NgStyle, OverlayPanelModule, DropdownModule, Checkbox, Popover, Slider, Select, Divider],
|
||||
imports: [Button, VirtualScrollerModule, BookCardComponent, AsyncPipe, ProgressSpinner, Menu, InputText, FormsModule, BookTableComponent, BookFilterComponent, Tooltip, NgClass, PrimeTemplate, NgStyle, OverlayPanelModule, DropdownModule, Checkbox, Popover, Slider, Select, Divider],
|
||||
providers: [SeriesCollapseFilter],
|
||||
animations: [
|
||||
trigger('slideInOut', [
|
||||
@@ -148,6 +147,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
|
||||
protected metadataMenuItems: MenuItem[] | undefined;
|
||||
currentBooks: Book[] = [];
|
||||
lastSelectedIndex: number | null = null;
|
||||
showFilter: boolean = false;
|
||||
|
||||
get currentCardSize() {
|
||||
return this.coverScalePreferenceService.currentCardSize;
|
||||
@@ -310,7 +310,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
|
||||
: VIEW_MODES.GRID)
|
||||
: (effectivePrefs.view?.toLowerCase() ?? VIEW_MODES.GRID);
|
||||
|
||||
this.bookFilterComponent.showFilters = sidebarParam === 'true' || (sidebarParam === null && this.filterVisibility);
|
||||
//this.showFilter = sidebarParam === 'true' || (sidebarParam === null && this.filterVisibility);
|
||||
|
||||
this.bookSorter.updateSortOptions();
|
||||
|
||||
@@ -323,7 +323,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
|
||||
[QUERY_PARAMS.VIEW]: this.currentViewMode,
|
||||
[QUERY_PARAMS.SORT]: this.bookSorter.selectedSort.field,
|
||||
[QUERY_PARAMS.DIRECTION]: this.bookSorter.selectedSort.direction === SortDirection.ASCENDING ? SORT_DIRECTION.ASCENDING : SORT_DIRECTION.DESCENDING,
|
||||
[QUERY_PARAMS.SIDEBAR]: this.bookFilterComponent.showFilters.toString(),
|
||||
[QUERY_PARAMS.SIDEBAR]: this.showFilter.toString(),
|
||||
[QUERY_PARAMS.FILTER]: Object.entries(parsedFilters).map(([k, v]) => `${k}:${v.join('|')}`).join(',')
|
||||
};
|
||||
|
||||
@@ -613,10 +613,10 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
toggleFilterSidebar() {
|
||||
this.bookFilterComponent.showFilters = !this.bookFilterComponent.showFilters;
|
||||
this.showFilter = !this.showFilter;
|
||||
this.router.navigate([], {
|
||||
queryParams: {
|
||||
[QUERY_PARAMS.SIDEBAR]: this.bookFilterComponent.showFilters.toString()
|
||||
[QUERY_PARAMS.SIDEBAR]: this.showFilter.toString()
|
||||
},
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<h4 class="book-title m-0 pl-2">{{ book.metadata?.title }}</h4>
|
||||
<p-tieredmenu #menu [model]="items" [popup]="true" appendTo="body"></p-tieredmenu>
|
||||
<p-button
|
||||
class="custom-button-padding"
|
||||
size="small"
|
||||
[text]="true"
|
||||
(click)="menu.toggle($event)"
|
||||
|
||||
@@ -176,3 +176,7 @@
|
||||
::ng-deep p-progressbar {
|
||||
--p-progressbar-border-radius: 0 !important;
|
||||
}
|
||||
|
||||
::ng-deep .custom-button-padding .p-button {
|
||||
padding-block: 0.25rem !important;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@if (showFilters) {
|
||||
<div class="w-[225px] ml-4 h-[calc(100vh-6.1rem)] rounded-xl bg-[var(--card-background)] overflow-hidden">
|
||||
@if (showFilter) {
|
||||
<div
|
||||
class="h-[calc(100vh-6.1rem)] rounded-xl bg-[var(--card-background)]">
|
||||
|
||||
<div class="flex items-center justify-between px-4 py-4">
|
||||
<span class="text font-semibold">Filter Mode</span>
|
||||
<p-selectButton
|
||||
@@ -11,42 +13,33 @@
|
||||
allowEmpty="false"
|
||||
styleClass="h-8"></p-selectButton>
|
||||
</div>
|
||||
|
||||
<p-accordion [value]="expandedPanels">
|
||||
@for (filterType of filterTypes; track filterType; let i = $index) {
|
||||
<p-accordion-panel [value]="i">
|
||||
<p-accordion-header>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
{{ filterLabels[filterType] || (filterType | titlecase) }}
|
||||
@if (activeFilters[filterType]?.length) {
|
||||
<span class="text-sm text-[var(--primary-color)]">
|
||||
({{ activeFilters[filterType].length }})
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
{{ filterLabels[filterType] || (filterType | titlecase) }}
|
||||
@if (activeFilters[filterType]?.length) {
|
||||
<span class="text-sm text-[var(--primary-color)]">
|
||||
({{ activeFilters[filterType].length }})
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</p-accordion-header>
|
||||
|
||||
<p-accordion-content class="h-[27.5rem] !overflow-y-auto">
|
||||
@if (filterStreams[filterType] | async; as filters) {
|
||||
<div>
|
||||
@for (filter of filters; track filter) {
|
||||
<div>
|
||||
<div
|
||||
class="cursor-pointer transition-colors duration-200 ease-in-out pb-1 flex items-center gap-2"
|
||||
[ngClass]="{
|
||||
'text-[var(--primary-color)]':
|
||||
activeFilters[filterType]?.includes(filter.value?.id || filter.value)
|
||||
}"
|
||||
(click)="handleFilterClick(filterType, filter.value?.id || filter.value)">
|
||||
<p-badge [value]="filter.bookCount"></p-badge>
|
||||
@if (filterType === 'amazonRating') {
|
||||
@if (filterType === 'amazonRating') {
|
||||
<span>{{ filter.value.name }}</span>
|
||||
} @else {
|
||||
{{ filter.value.name || filter.value }}
|
||||
}
|
||||
} @else {
|
||||
{{ filter.value.name || filter.value }}
|
||||
}
|
||||
</div>
|
||||
<div class="cursor-pointer transition-colors duration-200 ease-in-out pb-1 flex items-center gap-2"
|
||||
[ngClass]="{
|
||||
'text-[var(--primary-color)]':
|
||||
activeFilters[filterType]?.includes(filter.value?.id || filter.value)
|
||||
}"
|
||||
(click)="handleFilterClick(filterType, filter.value?.id || filter.value)">
|
||||
<p-badge [value]="filter.bookCount"></p-badge>
|
||||
{{ filter.value.name || filter.value }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -147,10 +147,10 @@ export class BookFilterComponent implements OnInit, OnDestroy {
|
||||
@Output() filterSelected = new EventEmitter<Record<string, any> | null>();
|
||||
@Output() filterModeChanged = new EventEmitter<'and' | 'or'>();
|
||||
|
||||
@Input() showFilters: boolean = true;
|
||||
@Input() entity$!: Observable<Library | Shelf | null> | undefined;
|
||||
@Input() entityType$!: Observable<EntityType> | undefined;
|
||||
@Input() resetFilter$!: Subject<void>;
|
||||
@Input() showFilter: boolean = false;
|
||||
|
||||
activeFilters: Record<string, any> = {};
|
||||
filterStreams: Record<string, Observable<Filter<any>[]>> = {};
|
||||
|
||||
@@ -65,14 +65,13 @@ export class SeriesCollapseFilter implements BookFilter {
|
||||
collapsedBooks.push(book);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const [seriesName, group] of seriesMap.entries()) {
|
||||
const sortedGroup = group.slice().sort((a, b) => {
|
||||
const aNum = a.metadata?.seriesNumber ?? Number.MAX_VALUE;
|
||||
const bNum = b.metadata?.seriesNumber ?? Number.MAX_VALUE;
|
||||
return aNum - bNum;
|
||||
});
|
||||
|
||||
const firstBook = sortedGroup[0];
|
||||
collapsedBooks.push({
|
||||
...firstBook,
|
||||
@@ -80,7 +79,7 @@ export class SeriesCollapseFilter implements BookFilter {
|
||||
});
|
||||
}
|
||||
|
||||
return { ...bookState, books: collapsedBooks };
|
||||
return {...bookState, books: collapsedBooks};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,86 @@
|
||||
<div class="ml-5 relative">
|
||||
<div class="w-[400px] relative">
|
||||
<i class="pi pi-search search-icon"></i>
|
||||
<div class="ml-2 md:ml-5 relative w-full max-w-[500px]">
|
||||
<!-- Desktop view -->
|
||||
<p-iconfield class="w-full sm:flex">
|
||||
<p-inputicon styleClass="pi pi-search"/>
|
||||
<input
|
||||
style="width: 400px;"
|
||||
type="text"
|
||||
pInputText
|
||||
[(ngModel)]="searchQuery"
|
||||
(input)="onSearchInputChange()"
|
||||
placeholder="Search books by title or author..."
|
||||
class="search-input"
|
||||
/>
|
||||
class="w-full pr-10"
|
||||
/>
|
||||
@if (searchQuery) {
|
||||
<p-button icon="pi pi-times" (click)="clearSearch()" [text]="true" [rounded]="true" class="clear-btn" aria-label="Clear Search"></p-button>
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
(click)="clearSearch()"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 hidden lg:inline-flex"
|
||||
aria-label="Clear Search">
|
||||
</p-button>
|
||||
}
|
||||
</p-iconfield>
|
||||
|
||||
<!--<div class="sm:hidden flex items-center">
|
||||
<p-button
|
||||
icon="pi pi-search"
|
||||
(click)="toggleSearchInputDropdown()"
|
||||
[rounded]="true">
|
||||
</p-button>
|
||||
</div>
|
||||
|
||||
<div class="search-dropdown layout-menu pt-4" [class.show]="(books.length > 0)">
|
||||
@if (books.length > 0) {
|
||||
@for (book of books; track book) {
|
||||
<div class="search-dropdown-item flex flex-col" (click)="onBookClick(book)">
|
||||
<div class="flex flex-row w-full px-2">
|
||||
<img [attr.src]="urlHelper.getCoverUrl(book.id, book.metadata?.coverUpdatedOn)" alt="Book Cover" class="search-book-cover"/>
|
||||
<div class="search-book-details">
|
||||
<div class="flex items-center">
|
||||
<p class="search-book-name text-lg font-medium truncate">{{ book.metadata?.title | slice:0:70 }}</p>
|
||||
</div>
|
||||
@if (book.metadata?.authors?.length ?? 0 > 0) {
|
||||
<div class="search-book-authors">
|
||||
<p class="italic">by: {{ getAuthorNames(book.metadata?.authors) }}</p>
|
||||
<!– Dropdown input for small screens –>
|
||||
@if (isSearchDropdownOpen) {
|
||||
<div class="absolute z-10 shadow-md rounded-md mt-2 w-56 sm:hidden">
|
||||
<input
|
||||
type="text"
|
||||
pInputText
|
||||
[(ngModel)]="searchQuery"
|
||||
(input)="onSearchInputChange()"
|
||||
(blur)="closeSearchDropdown()"
|
||||
placeholder="Search books..."
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
}-->
|
||||
|
||||
<!-- Search results below small screen search input -->
|
||||
@if (books.length > 0 || searchQuery) {
|
||||
<div
|
||||
class="search-dropdown layout-menu w-[350px] sm:w-[600px]"
|
||||
[class.show]="(books.length > 0)"
|
||||
>
|
||||
@if (books.length > 0) {
|
||||
@for (book of books; track book) {
|
||||
<div class="search-dropdown-item flex flex-col" (click)="onBookClick(book)">
|
||||
<div class="flex flex-row w-full px-2">
|
||||
<img
|
||||
[attr.src]="urlHelper.getCoverUrl(book.id, book.metadata?.coverUpdatedOn)"
|
||||
alt="Book Cover"
|
||||
class="search-book-cover"
|
||||
/>
|
||||
<div class="search-book-details">
|
||||
<div class="flex items-center">
|
||||
<p class="search-book-name text-lg font-medium truncate">
|
||||
{{ book.metadata?.title | slice: 0:70 }}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@if (book.metadata?.authors?.length ?? 0 > 0) {
|
||||
<div class="search-book-authors">
|
||||
<p class="italic">by: {{ getAuthorNames(book.metadata?.authors) }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p-divider></p-divider>
|
||||
</div>
|
||||
<p-divider></p-divider>
|
||||
}
|
||||
} @else {
|
||||
<div class="search-dropdown-item px-4 py-2">
|
||||
<span>No results found</span>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="search-dropdown-item">
|
||||
<span>No results found</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
z-index: 1000;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
width: 600px;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
transition: opacity 0.3s ease, height 0.3s ease;
|
||||
|
||||
@@ -6,10 +6,13 @@ import {FormsModule} from '@angular/forms';
|
||||
import {InputTextModule} from 'primeng/inputtext';
|
||||
import {BookService} from '../../service/book.service';
|
||||
import {Button} from 'primeng/button';
|
||||
import { SlicePipe } from '@angular/common';
|
||||
import {SlicePipe} from '@angular/common';
|
||||
import {Divider} from 'primeng/divider';
|
||||
import {UrlHelperService} from '../../../utilities/service/url-helper.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {OverlayPanelModule} from 'primeng/overlaypanel';
|
||||
import {IconField} from 'primeng/iconfield';
|
||||
import {InputIcon} from 'primeng/inputicon';
|
||||
|
||||
@Component({
|
||||
selector: 'app-book-searcher',
|
||||
@@ -19,8 +22,11 @@ import {Router} from '@angular/router';
|
||||
InputTextModule,
|
||||
Button,
|
||||
SlicePipe,
|
||||
Divider
|
||||
],
|
||||
Divider,
|
||||
OverlayPanelModule,
|
||||
IconField,
|
||||
InputIcon
|
||||
],
|
||||
styleUrls: ['./book-searcher.component.scss'],
|
||||
standalone: true
|
||||
})
|
||||
@@ -34,6 +40,16 @@ export class BookSearcherComponent implements OnInit, OnDestroy {
|
||||
private router = inject(Router);
|
||||
protected urlHelper = inject(UrlHelperService);
|
||||
|
||||
isSearchDropdownOpen = false;
|
||||
|
||||
toggleSearchInputDropdown() {
|
||||
this.isSearchDropdownOpen = !this.isSearchDropdownOpen;
|
||||
}
|
||||
|
||||
closeSearchDropdown() {
|
||||
this.isSearchDropdownOpen = false;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initializeSearch();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="flex flex-col gap-10 p-4 items-center justify-center min-w-[500px] max-w-[700px] w-full">
|
||||
<div class="flex flex-col gap-10 p-4 items-center justify-center min-w-[300px] max-w-[700px] sm:min-w-[400px] mx-auto">
|
||||
|
||||
<p-select
|
||||
[options]="emailProviders"
|
||||
|
||||
@@ -1,82 +1,114 @@
|
||||
<p-stepper [value]="1">
|
||||
<p-step-list>
|
||||
<p-step [value]="1">Library Details</p-step>
|
||||
<p-step [value]="2">Select Directory</p-step>
|
||||
</p-step-list>
|
||||
<p-step-panels>
|
||||
<p-step-panel [value]="1">
|
||||
<ng-template #content let-activateCallback="activateCallback">
|
||||
<div class="flex flex-col w-[40rem] h-[25rem]">
|
||||
<div class="flex flex-auto justify-center items-center font-medium border-[var(--border-color)] border-2 border-dashed">
|
||||
<div class="flex flex-col gap-10">
|
||||
<div class="flex items-center gap-8">
|
||||
<p>Library Name: </p>
|
||||
<input type="text" pInputText [(ngModel)]="chosenLibraryName" placeholder="Enter library name..."/>
|
||||
</div>
|
||||
<div class="flex items-center gap-11">
|
||||
<p>Library Icon:</p>
|
||||
<div *ngIf="!selectedIcon">
|
||||
<p-button label="Select Icon" icon="pi pi-search" (onClick)="openIconPicker()"></p-button>
|
||||
</div>
|
||||
<div *ngIf="selectedIcon">
|
||||
<i [class]="selectedIcon" class="text-xl mr-3"></i>
|
||||
<p-button icon="pi pi-times" (onClick)="clearSelectedIcon()" [rounded]="true" [text]="true" [outlined]="true" severity="danger"></p-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<p>Monitor Folders:</p>
|
||||
<p-toggleswitch [(ngModel)]="watch" />
|
||||
<i class="pi pi-info-circle text-sky-600"
|
||||
pTooltip="Toggle this to enable or disable folder monitoring. When enabled, the system will watch specified folders for file changes. Books are added and removed automatically based on files added or removed in the library folders."
|
||||
tooltipPosition="right"
|
||||
style="cursor: pointer;">
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
<app-icon-picker (iconSelected)="onIconSelected($event)"></app-icon-picker>
|
||||
</div>
|
||||
<div class="flex pt-6 justify-end">
|
||||
<p-button label="Next" icon="pi pi-arrow-right" iconPos="right" [disabled]="!isLibraryDetailsValid()" (onClick)="validateLibraryNameAndProceed(activateCallback)" />
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-step-panel>
|
||||
<div class="w-full flex justify-center">
|
||||
<p-stepper [value]="1">
|
||||
<p-step-list>
|
||||
<p-step [value]="1">Library Details</p-step>
|
||||
<p-step [value]="2">Select Directories</p-step>
|
||||
</p-step-list>
|
||||
|
||||
<p-step-panel [value]="2">
|
||||
<ng-template #content let-activateCallback="activateCallback">
|
||||
<div class="flex flex-col w-[40rem] h-[25rem]">
|
||||
<div class="flex flex-auto justify-center items-center font-medium border-[var(--border-color)] border-2 border-dashed">
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<p-step-panels>
|
||||
|
||||
<p-step-panel [value]="1">
|
||||
<ng-template #content let-activateCallback="activateCallback">
|
||||
<div class="flex flex-col justify-between w-full md:w-[40rem] max-w-[50rem] mx-auto">
|
||||
|
||||
<div class="flex-grow flex items-center justify-center">
|
||||
<div class="grid grid-cols-[auto,1fr] gap-x-6 gap-y-10 border border-[var(--border-color)] p-6 rounded-xl w-full">
|
||||
|
||||
<label class="self-center">Library Name</label>
|
||||
<input
|
||||
pInputText
|
||||
class="w-full"
|
||||
[(ngModel)]="chosenLibraryName"
|
||||
placeholder="Enter library name..." />
|
||||
|
||||
<label class="self-center">Library Icon</label>
|
||||
@if (!selectedIcon) {
|
||||
<p-button label="Select Icon" icon="pi pi-search" (onClick)="openIconPicker()" />
|
||||
} @else {
|
||||
<div class="flex items-center gap-2">
|
||||
<i [class]="selectedIcon" class="text-3xl"></i>
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
(onClick)="clearSelectedIcon()"
|
||||
[rounded]="true"
|
||||
[text]="true"
|
||||
[outlined]="true"
|
||||
severity="danger" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<label class="self-center">Monitor Folders</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<p-toggleswitch [(ngModel)]="watch" />
|
||||
<i
|
||||
class="pi pi-info-circle text-sky-600 cursor-pointer"
|
||||
pTooltip="Watches for file changes and auto-adds/removes books."
|
||||
tooltipPosition="right"></i>
|
||||
</div>
|
||||
</div>
|
||||
<app-icon-picker (iconSelected)="onIconSelected($event)"></app-icon-picker>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-6">
|
||||
<p-button
|
||||
severity="info"
|
||||
label="Add book folders"
|
||||
[outlined]="true"
|
||||
(onClick)="openDirectoryPicker()">
|
||||
</p-button>
|
||||
<p-table [value]="folders" class="mt-4 max-w-[35rem] max-h-[11.5rem] !overflow-scroll">
|
||||
<ng-template pTemplate="body" let-folder let-i="rowIndex">
|
||||
<tr>
|
||||
<td class="!p-1 !px-2 !border-0">{{ folder }}</td>
|
||||
<td class="!p-1 !px-2 !border-0">
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
[rounded]="true"
|
||||
[text]="true"
|
||||
severity="danger"
|
||||
(onClick)="removeFolder(i)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
label="Next"
|
||||
icon="pi pi-arrow-right"
|
||||
iconPos="right"
|
||||
(onClick)="validateLibraryNameAndProceed(activateCallback)"
|
||||
[disabled]="!isLibraryDetailsValid()" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex pt-6 justify-between">
|
||||
<p-button label="Back" icon="pi pi-arrow-left" iconPos="right" (onClick)="activateCallback(1)" />
|
||||
<p-button severity="success" label="Save" icon="pi pi-save" [disabled]="!isDirectorySelectionValid()" (onClick)="createOrUpdateLibrary()"></p-button>
|
||||
</ng-template>
|
||||
</p-step-panel>
|
||||
|
||||
<p-step-panel [value]="2">
|
||||
<ng-template #content let-activateCallback="activateCallback">
|
||||
<div class="flex flex-col justify-between w-[24rem] md:w-[40rem] max-w-[50rem] mx-auto">
|
||||
|
||||
<div class="flex-grow flex items-center justify-center">
|
||||
<div class="flex flex-col gap-6 border border-[var(--border-color)] px-2 py-4 md:p-6 rounded-xl w-full">
|
||||
|
||||
<div class="flex justify-center">
|
||||
<p-button
|
||||
label="Add Book Folders"
|
||||
icon="pi pi-folder-open"
|
||||
[outlined]="true"
|
||||
severity="info"
|
||||
(onClick)="openDirectoryPicker()" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 max-h-70 overflow-y-auto">
|
||||
@for (folder of folders; track folder) {
|
||||
<div class="flex items-start justify-between p-1 gap-4">
|
||||
<div class="flex items-center gap-4 overflow-x-auto whitespace-nowrap flex-1 pr-2" [pTooltip]="folder" tooltipPosition="top">
|
||||
<i class="pi pi-folder text-yellow-600 text-sm"></i>
|
||||
<span class="truncate">{{ folder }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0">
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
[rounded]="true"
|
||||
outlined
|
||||
size="small"
|
||||
severity="danger"
|
||||
(onClick)="removeFolder(folders.indexOf(folder))" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between pt-6">
|
||||
<p-button label="Back" icon="pi pi-arrow-left" iconPos="left" (onClick)="activateCallback(1)" />
|
||||
<p-button label="Save" icon="pi pi-save" severity="success" [disabled]="!isDirectorySelectionValid()" (onClick)="createOrUpdateLibrary()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-step-panel>
|
||||
</p-step-panels>
|
||||
</p-stepper>
|
||||
</ng-template>
|
||||
</p-step-panel>
|
||||
|
||||
</p-step-panels>
|
||||
</p-stepper>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ import {IconPickerComponent} from '../../../utilities/component/icon-picker/icon
|
||||
import {Button} from 'primeng/button';
|
||||
import {TableModule} from 'primeng/table';
|
||||
import {Step, StepList, StepPanel, StepPanels, Stepper} from 'primeng/stepper';
|
||||
import {NgIf} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {InputText} from 'primeng/inputtext';
|
||||
import {Library} from '../../model/library.model';
|
||||
@@ -19,7 +18,7 @@ import {Tooltip} from 'primeng/tooltip';
|
||||
selector: 'app-library-creator',
|
||||
standalone: true,
|
||||
templateUrl: './library-creator.component.html',
|
||||
imports: [Button, TableModule, StepPanel, IconPickerComponent, NgIf, FormsModule, InputText, Stepper, StepList, Step, StepPanels, ToggleSwitch, Tooltip],
|
||||
imports: [Button, TableModule, StepPanel, IconPickerComponent, FormsModule, InputText, Stepper, StepList, Step, StepPanels, ToggleSwitch, Tooltip],
|
||||
styleUrl: './library-creator.component.scss'
|
||||
})
|
||||
export class LibraryCreatorComponent implements OnInit {
|
||||
@@ -84,6 +83,7 @@ export class LibraryCreatorComponent implements OnInit {
|
||||
}
|
||||
|
||||
openIconPicker() {
|
||||
console.log('aaa')
|
||||
if (this.iconPicker) {
|
||||
this.iconPicker.open();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="rounded-xl overflow-hidden">
|
||||
<p-tabs [value]="tab" lazy="true">
|
||||
<p-tabs [value]="tab" lazy="true" scrollable >
|
||||
<p-tablist>
|
||||
<p-tab value="view">
|
||||
<i [class]="'pi pi-book'"></i>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@if (book$ | async; as book) {
|
||||
<form [formGroup]="metadataForm" (ngSubmit)="onSave()" class="flex flex-col h-full">
|
||||
<form [formGroup]="metadataForm" (ngSubmit)="onSave()" class="flex flex-col h-full w-full">
|
||||
<div class="flex-grow overflow-auto p-4">
|
||||
<div class="flex justify-center items-center" [ngStyle]="{'padding-bottom': '0.75rem', 'padding-left': '7.6rem'}">
|
||||
<p class="ml-3 text-lg">Current Metadata</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@if (selectedFetchedMetadata$ | async; as selectedFetchedMetadata) {
|
||||
<div class="h-full flex-auto">
|
||||
<div class="h-full w-full flex-auto min-w-[80rem]">
|
||||
<app-metadata-picker
|
||||
[fetchedMetadata]="selectedFetchedMetadata"
|
||||
[book$]="book$"
|
||||
@@ -7,7 +7,7 @@
|
||||
</app-metadata-picker>
|
||||
</div>
|
||||
} @else {
|
||||
<form class="flex flex-col w-full h-full pt-2" [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
<form class="flex flex-col w-full h-full pt-2 min-w-[60rem] pr-4" [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
<div class="flex w-full gap-x-4 items-end">
|
||||
<div class="flex flex-col gap-1 w-3/12">
|
||||
<label>Providers</label>
|
||||
|
||||
@@ -313,7 +313,7 @@
|
||||
|
||||
<p-divider></p-divider>
|
||||
|
||||
<div class="flex-1 space-y-2 px-2">
|
||||
<div class="flex-1 space-y-2 px-2 min-w-[40rem]">
|
||||
<div [ngClass]="{ 'line-clamp-5': !isExpanded, 'line-clamp-none': isExpanded }" class="transition-all duration-300 overflow-hidden description-container">
|
||||
<div class="readonly-editor px-2">
|
||||
<p-editor #quillEditor [readonly]="true" [style]="{height: '250px'}" [(ngModel)]="book.metadata!.description">
|
||||
|
||||
@@ -96,7 +96,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges {
|
||||
header: 'Send Book to Email',
|
||||
modal: true,
|
||||
closable: true,
|
||||
style: {position: 'absolute', top: '15%'},
|
||||
style: {position: 'absolute', top: '20%'},
|
||||
data: {bookId: metadata.bookId}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,17 +75,19 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="flex gap-10">
|
||||
<div class="flex flex-row items-center gap-4 justify-start">
|
||||
<p>Refresh covers: </p>
|
||||
<div class="flex flex-row items-center justify-between gap-4 w-full">
|
||||
|
||||
<div class="flex flex-row gap-10">
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<p>Refresh covers:</p>
|
||||
<p-checkbox [(ngModel)]="refreshCovers" [binary]="true"></p-checkbox>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-4 justify-start">
|
||||
<p>Merge categories: </p>
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<p>Merge categories:</p>
|
||||
<p-checkbox [(ngModel)]="mergeCategories" [binary]="true"></p-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-6">
|
||||
<p-button severity="warn" outlined="true" label="Reset" (onClick)="reset()"></p-button>
|
||||
<p-button [label]="submitButtonLabel" outlined="true" (onClick)="submit()"></p-button>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
style="background: linear-gradient(60deg, var(--primary-color) 0%, #1e3a8a 100%);">
|
||||
<div class="flex flex-col items-center justify-center -mt-48">
|
||||
<div style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
|
||||
<div class="w-full bg-surface-0 dark:bg-surface-900 py-16 px-8 sm:px-16" style="border-radius: 53px">
|
||||
<div class="w-full bg-surface-0 dark:bg-surface-900 pt-8 pb-12 px-8 sm:px-8" style="border-radius: 53px">
|
||||
<div class="text-center mb-8">
|
||||
<svg class="mb-8 w-16 h-16 mx-auto" viewBox="0 0 126 126" fill="var(--primary-color)" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="mb-8 w-14 h-14 mx-auto" viewBox="0 0 126 126" fill="var(--primary-color)" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
|
||||
<path
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<form class="flex flex-col gap-4" #loginForm="ngForm" (ngSubmit)="login()">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="username" class="text-xl font-medium">Username</label>
|
||||
<label for="username" class="text-lg font-medium">Username</label>
|
||||
<input
|
||||
pInputText
|
||||
id="username"
|
||||
@@ -25,12 +25,12 @@
|
||||
required
|
||||
[(ngModel)]="username"
|
||||
placeholder="Enter your username"
|
||||
class="w-full md:w-[30rem]"
|
||||
class="w-full md:w-[27rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="password" class="text-xl font-medium">Password</label>
|
||||
<label for="password" class="text-lg font-medium">Password</label>
|
||||
<p-password
|
||||
id="password"
|
||||
name="password"
|
||||
|
||||
@@ -30,4 +30,23 @@
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
flex-basis: 135px;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.dashboard-scroller-title {
|
||||
font-size: 1.25rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.dashboard-scroller-card {
|
||||
height: 200px;
|
||||
width: 120px;
|
||||
flex-basis: 110px;
|
||||
}
|
||||
|
||||
.dashboard-scroller-infinite {
|
||||
gap: 1rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Component, OnDestroy, Renderer2, ViewChild } from '@angular/core';
|
||||
import {Component, OnDestroy, Renderer2, ViewChild} from '@angular/core';
|
||||
import {NavigationEnd, Router, RouterOutlet} from '@angular/router';
|
||||
import { filter, Subscription } from 'rxjs';
|
||||
import { LayoutService } from "./service/app.layout.service";
|
||||
import { AppSidebarComponent } from "../layout-sidebar/app.sidebar.component";
|
||||
import { AppTopBarComponent } from '../layout-topbar/app.topbar.component';
|
||||
import {filter, Subscription} from 'rxjs';
|
||||
import {LayoutService} from "./service/app.layout.service";
|
||||
import {AppSidebarComponent} from "../layout-sidebar/app.sidebar.component";
|
||||
import {AppTopBarComponent} from '../layout-topbar/app.topbar.component';
|
||||
import {NgClass} from '@angular/common';
|
||||
import {ToastModule} from 'primeng/toast';
|
||||
|
||||
@Component({
|
||||
selector: 'app-layout',
|
||||
selector: 'app-layout',
|
||||
imports: [
|
||||
RouterOutlet,
|
||||
AppSidebarComponent,
|
||||
@@ -16,115 +16,104 @@ import {ToastModule} from 'primeng/toast';
|
||||
NgClass,
|
||||
ToastModule
|
||||
],
|
||||
templateUrl: './app.layout.component.html'
|
||||
templateUrl: './app.layout.component.html'
|
||||
})
|
||||
export class AppLayoutComponent implements OnDestroy {
|
||||
|
||||
overlayMenuOpenSubscription: Subscription;
|
||||
overlayMenuOpenSubscription: Subscription;
|
||||
|
||||
menuOutsideClickListener: any;
|
||||
menuOutsideClickListener: any;
|
||||
|
||||
profileMenuOutsideClickListener: any;
|
||||
profileMenuOutsideClickListener: any;
|
||||
|
||||
@ViewChild(AppSidebarComponent) appSidebar!: AppSidebarComponent;
|
||||
@ViewChild(AppSidebarComponent) appSidebar!: AppSidebarComponent;
|
||||
|
||||
@ViewChild(AppTopBarComponent) appTopbar!: AppTopBarComponent;
|
||||
@ViewChild(AppTopBarComponent) appTopbar!: AppTopBarComponent;
|
||||
|
||||
constructor(public layoutService: LayoutService, public renderer: Renderer2, public router: Router) {
|
||||
this.overlayMenuOpenSubscription = this.layoutService.overlayOpen$.subscribe(() => {
|
||||
if (!this.menuOutsideClickListener) {
|
||||
this.menuOutsideClickListener = this.renderer.listen('document', 'click', event => {
|
||||
const isOutsideClicked = !(this.appSidebar.el.nativeElement.isSameNode(event.target) || this.appSidebar.el.nativeElement.contains(event.target)
|
||||
|| this.appTopbar.menuButton.nativeElement.isSameNode(event.target) || this.appTopbar.menuButton.nativeElement.contains(event.target));
|
||||
|
||||
if (isOutsideClicked) {
|
||||
this.hideMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.profileMenuOutsideClickListener) {
|
||||
this.profileMenuOutsideClickListener = this.renderer.listen('document', 'click', event => {
|
||||
const isOutsideClicked = !(this.appTopbar.menu.nativeElement.isSameNode(event.target) || this.appTopbar.menu.nativeElement.contains(event.target)
|
||||
|| this.appTopbar.topbarMenuButton.nativeElement.isSameNode(event.target) || this.appTopbar.topbarMenuButton.nativeElement.contains(event.target));
|
||||
|
||||
if (isOutsideClicked) {
|
||||
this.hideProfileMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.layoutService.state.staticMenuMobileActive) {
|
||||
this.blockBodyScroll();
|
||||
}
|
||||
constructor(public layoutService: LayoutService, public renderer: Renderer2, public router: Router) {
|
||||
this.overlayMenuOpenSubscription = this.layoutService.overlayOpen$.subscribe(() => {
|
||||
if (!this.menuOutsideClickListener) {
|
||||
this.menuOutsideClickListener = this.renderer.listen('document', 'click', (event) => {
|
||||
if (this.isOutsideClicked(event)) {
|
||||
this.hideMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.router.events.pipe(filter(event => event instanceof NavigationEnd))
|
||||
.subscribe(() => {
|
||||
this.hideMenu();
|
||||
this.hideProfileMenu();
|
||||
});
|
||||
if (this.layoutService.state.staticMenuMobileActive) {
|
||||
this.blockBodyScroll();
|
||||
}
|
||||
});
|
||||
|
||||
this.router.events.pipe(filter(event => event instanceof NavigationEnd))
|
||||
.subscribe(() => {
|
||||
this.hideMenu();
|
||||
this.hideProfileMenu();
|
||||
});
|
||||
}
|
||||
|
||||
isOutsideClicked(event: MouseEvent): boolean {
|
||||
const sidebarEl = document.querySelector('.layout-sidebar');
|
||||
const topbarEl = document.querySelector('.layout-menu-button');
|
||||
const eventTarget = event.target as Node;
|
||||
const clickedInsideSidebar = sidebarEl?.isSameNode(eventTarget) || sidebarEl?.contains(eventTarget);
|
||||
const clickedInsideTopbar = topbarEl?.isSameNode(eventTarget) || topbarEl?.contains(eventTarget);
|
||||
return !(clickedInsideSidebar || clickedInsideTopbar);
|
||||
}
|
||||
|
||||
hideMenu() {
|
||||
this.layoutService.state.overlayMenuActive = false;
|
||||
this.layoutService.state.staticMenuMobileActive = false;
|
||||
this.layoutService.state.menuHoverActive = false;
|
||||
if (this.menuOutsideClickListener) {
|
||||
this.menuOutsideClickListener();
|
||||
this.menuOutsideClickListener = null;
|
||||
}
|
||||
this.unblockBodyScroll();
|
||||
}
|
||||
|
||||
hideProfileMenu() {
|
||||
this.layoutService.state.profileSidebarVisible = false;
|
||||
if (this.profileMenuOutsideClickListener) {
|
||||
this.profileMenuOutsideClickListener();
|
||||
this.profileMenuOutsideClickListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
blockBodyScroll(): void {
|
||||
if (document.body.classList) {
|
||||
document.body.classList.add('blocked-scroll');
|
||||
} else {
|
||||
document.body.className += ' blocked-scroll';
|
||||
}
|
||||
}
|
||||
|
||||
unblockBodyScroll(): void {
|
||||
if (document.body.classList) {
|
||||
document.body.classList.remove('blocked-scroll');
|
||||
} else {
|
||||
document.body.className = document.body.className.replace(new RegExp('(^|\\b)' +
|
||||
'blocked-scroll'.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
||||
}
|
||||
}
|
||||
|
||||
get containerClass() {
|
||||
return {
|
||||
'layout-overlay': this.layoutService.config().menuMode === 'overlay',
|
||||
'layout-static': this.layoutService.config().menuMode === 'static',
|
||||
'layout-static-inactive': this.layoutService.state.staticMenuDesktopInactive && this.layoutService.config().menuMode === 'static',
|
||||
'layout-overlay-active': this.layoutService.state.overlayMenuActive,
|
||||
'layout-mobile-active': this.layoutService.state.staticMenuMobileActive
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.overlayMenuOpenSubscription) {
|
||||
this.overlayMenuOpenSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
hideMenu() {
|
||||
this.layoutService.state.overlayMenuActive = false;
|
||||
this.layoutService.state.staticMenuMobileActive = false;
|
||||
this.layoutService.state.menuHoverActive = false;
|
||||
if (this.menuOutsideClickListener) {
|
||||
this.menuOutsideClickListener();
|
||||
this.menuOutsideClickListener = null;
|
||||
}
|
||||
this.unblockBodyScroll();
|
||||
}
|
||||
|
||||
hideProfileMenu() {
|
||||
this.layoutService.state.profileSidebarVisible = false;
|
||||
if (this.profileMenuOutsideClickListener) {
|
||||
this.profileMenuOutsideClickListener();
|
||||
this.profileMenuOutsideClickListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
blockBodyScroll(): void {
|
||||
if (document.body.classList) {
|
||||
document.body.classList.add('blocked-scroll');
|
||||
}
|
||||
else {
|
||||
document.body.className += ' blocked-scroll';
|
||||
}
|
||||
}
|
||||
|
||||
unblockBodyScroll(): void {
|
||||
if (document.body.classList) {
|
||||
document.body.classList.remove('blocked-scroll');
|
||||
}
|
||||
else {
|
||||
document.body.className = document.body.className.replace(new RegExp('(^|\\b)' +
|
||||
'blocked-scroll'.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
||||
}
|
||||
}
|
||||
|
||||
get containerClass() {
|
||||
return {
|
||||
'layout-theme-light': this.layoutService.config().colorScheme === 'light',
|
||||
'layout-theme-dark': this.layoutService.config().colorScheme === 'dark',
|
||||
'layout-overlay': this.layoutService.config().menuMode === 'overlay',
|
||||
'layout-static': this.layoutService.config().menuMode === 'static',
|
||||
'layout-static-inactive': this.layoutService.state.staticMenuDesktopInactive && this.layoutService.config().menuMode === 'static',
|
||||
'layout-overlay-active': this.layoutService.state.overlayMenuActive,
|
||||
'layout-mobile-active': this.layoutService.state.staticMenuMobileActive,
|
||||
'p-input-filled': this.layoutService.config().inputStyle === 'filled',
|
||||
'p-ripple-disabled': !this.layoutService.config().ripple
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.overlayMenuOpenSubscription) {
|
||||
this.overlayMenuOpenSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
if (this.menuOutsideClickListener) {
|
||||
this.menuOutsideClickListener();
|
||||
}
|
||||
if (this.menuOutsideClickListener) {
|
||||
this.menuOutsideClickListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,144 +1,138 @@
|
||||
import { Injectable, effect, signal } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import {Injectable, effect, signal} from '@angular/core';
|
||||
import {Subject} from 'rxjs';
|
||||
|
||||
export interface AppConfig {
|
||||
inputStyle: string;
|
||||
colorScheme: string;
|
||||
theme: string;
|
||||
ripple: boolean;
|
||||
menuMode: string;
|
||||
scale: number;
|
||||
inputStyle: string;
|
||||
colorScheme: string;
|
||||
theme: string;
|
||||
ripple: boolean;
|
||||
menuMode: string;
|
||||
scale: number;
|
||||
}
|
||||
|
||||
interface LayoutState {
|
||||
staticMenuDesktopInactive: boolean;
|
||||
overlayMenuActive: boolean;
|
||||
profileSidebarVisible: boolean;
|
||||
configSidebarVisible: boolean;
|
||||
staticMenuMobileActive: boolean;
|
||||
menuHoverActive: boolean;
|
||||
staticMenuDesktopInactive: boolean;
|
||||
overlayMenuActive: boolean;
|
||||
profileSidebarVisible: boolean;
|
||||
configSidebarVisible: boolean;
|
||||
staticMenuMobileActive: boolean;
|
||||
menuHoverActive: boolean;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LayoutService {
|
||||
_config: AppConfig = {
|
||||
ripple: false,
|
||||
inputStyle: 'outlined',
|
||||
menuMode: 'static',
|
||||
colorScheme: 'light',
|
||||
theme: 'lara-light-indigo',
|
||||
scale: 14,
|
||||
};
|
||||
_config: AppConfig = {
|
||||
ripple: false,
|
||||
inputStyle: 'outlined',
|
||||
menuMode: 'static',
|
||||
colorScheme: 'light',
|
||||
theme: 'lara-light-indigo',
|
||||
scale: 14,
|
||||
};
|
||||
|
||||
config = signal<AppConfig>(this._config);
|
||||
config = signal<AppConfig>(this._config);
|
||||
|
||||
state: LayoutState = {
|
||||
staticMenuDesktopInactive: false,
|
||||
overlayMenuActive: false,
|
||||
profileSidebarVisible: false,
|
||||
configSidebarVisible: false,
|
||||
staticMenuMobileActive: false,
|
||||
menuHoverActive: false,
|
||||
};
|
||||
state: LayoutState = {
|
||||
staticMenuDesktopInactive: false,
|
||||
overlayMenuActive: false,
|
||||
profileSidebarVisible: false,
|
||||
configSidebarVisible: false,
|
||||
staticMenuMobileActive: false,
|
||||
menuHoverActive: false,
|
||||
};
|
||||
|
||||
private configUpdate = new Subject<AppConfig>();
|
||||
private configUpdate = new Subject<AppConfig>();
|
||||
private overlayOpen = new Subject<any>();
|
||||
overlayOpen$ = this.overlayOpen.asObservable();
|
||||
|
||||
private overlayOpen = new Subject<any>();
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const config = this.config();
|
||||
if (this.updateStyle(config)) {
|
||||
this.changeTheme();
|
||||
}
|
||||
this.changeScale(config.scale);
|
||||
this.onConfigUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
configUpdate$ = this.configUpdate.asObservable();
|
||||
updateStyle(config: AppConfig) {
|
||||
return (
|
||||
config.theme !== this._config.theme ||
|
||||
config.colorScheme !== this._config.colorScheme
|
||||
);
|
||||
}
|
||||
|
||||
overlayOpen$ = this.overlayOpen.asObservable();
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const config = this.config();
|
||||
if (this.updateStyle(config)) {
|
||||
this.changeTheme();
|
||||
}
|
||||
this.changeScale(config.scale);
|
||||
this.onConfigUpdate();
|
||||
});
|
||||
onMenuToggle() {
|
||||
if (this.isOverlay()) {
|
||||
this.state.overlayMenuActive = !this.state.overlayMenuActive;
|
||||
if (this.state.overlayMenuActive) {
|
||||
this.overlayOpen.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
updateStyle(config: AppConfig) {
|
||||
return (
|
||||
config.theme !== this._config.theme ||
|
||||
config.colorScheme !== this._config.colorScheme
|
||||
);
|
||||
if (this.isDesktop()) {
|
||||
this.state.staticMenuDesktopInactive = !this.state.staticMenuDesktopInactive;
|
||||
} else {
|
||||
this.state.staticMenuMobileActive = !this.state.staticMenuMobileActive;
|
||||
if (this.state.staticMenuMobileActive) {
|
||||
this.overlayOpen.next(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMenuToggle() {
|
||||
if (this.isOverlay()) {
|
||||
this.state.overlayMenuActive = !this.state.overlayMenuActive;
|
||||
if (this.state.overlayMenuActive) {
|
||||
this.overlayOpen.next(null);
|
||||
}
|
||||
}
|
||||
isOverlay() {
|
||||
return this.config().menuMode === 'overlay';
|
||||
}
|
||||
|
||||
if (this.isDesktop()) {
|
||||
this.state.staticMenuDesktopInactive =
|
||||
!this.state.staticMenuDesktopInactive;
|
||||
} else {
|
||||
this.state.staticMenuMobileActive =
|
||||
!this.state.staticMenuMobileActive;
|
||||
isDesktop() {
|
||||
return window.innerWidth > 991;
|
||||
}
|
||||
|
||||
if (this.state.staticMenuMobileActive) {
|
||||
this.overlayOpen.next(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
onConfigUpdate() {
|
||||
this._config = {...this.config()};
|
||||
this.configUpdate.next(this.config());
|
||||
}
|
||||
|
||||
isOverlay() {
|
||||
return this.config().menuMode === 'overlay';
|
||||
}
|
||||
changeTheme() {
|
||||
const config = this.config();
|
||||
const themeLink = document.getElementById('theme-css') as HTMLLinkElement;
|
||||
const themeLinkHref = themeLink.getAttribute('href')!;
|
||||
const newHref = themeLinkHref
|
||||
.split('/')
|
||||
.map((el) =>
|
||||
el == this._config.theme
|
||||
? (el = config.theme)
|
||||
: el == `theme-${this._config.colorScheme}`
|
||||
? (el = `theme-${config.colorScheme}`)
|
||||
: el
|
||||
)
|
||||
.join('/');
|
||||
|
||||
isDesktop() {
|
||||
return window.innerWidth > 991;
|
||||
}
|
||||
this.replaceThemeLink(newHref);
|
||||
}
|
||||
|
||||
onConfigUpdate() {
|
||||
this._config = { ...this.config() };
|
||||
this.configUpdate.next(this.config());
|
||||
}
|
||||
replaceThemeLink(href: string) {
|
||||
const id = 'theme-css';
|
||||
const themeLink = document.getElementById(id) as HTMLLinkElement;
|
||||
const cloneLinkElement = themeLink.cloneNode(true) as HTMLLinkElement;
|
||||
|
||||
changeTheme() {
|
||||
const config = this.config();
|
||||
const themeLink = document.getElementById('theme-css') as HTMLLinkElement;
|
||||
const themeLinkHref = themeLink.getAttribute('href')!;
|
||||
const newHref = themeLinkHref
|
||||
.split('/')
|
||||
.map((el) =>
|
||||
el == this._config.theme
|
||||
? (el = config.theme)
|
||||
: el == `theme-${this._config.colorScheme}`
|
||||
? (el = `theme-${config.colorScheme}`)
|
||||
: el
|
||||
)
|
||||
.join('/');
|
||||
cloneLinkElement.setAttribute('href', href);
|
||||
cloneLinkElement.setAttribute('id', id + '-clone');
|
||||
|
||||
this.replaceThemeLink(newHref);
|
||||
}
|
||||
replaceThemeLink(href: string) {
|
||||
const id = 'theme-css';
|
||||
const themeLink = document.getElementById(id) as HTMLLinkElement;
|
||||
const cloneLinkElement = themeLink.cloneNode(true) as HTMLLinkElement;
|
||||
themeLink.parentNode!.insertBefore(
|
||||
cloneLinkElement,
|
||||
themeLink.nextSibling
|
||||
);
|
||||
cloneLinkElement.addEventListener('load', () => {
|
||||
themeLink.remove();
|
||||
cloneLinkElement.setAttribute('id', id);
|
||||
});
|
||||
}
|
||||
|
||||
cloneLinkElement.setAttribute('href', href);
|
||||
cloneLinkElement.setAttribute('id', id + '-clone');
|
||||
|
||||
themeLink.parentNode!.insertBefore(
|
||||
cloneLinkElement,
|
||||
themeLink.nextSibling
|
||||
);
|
||||
cloneLinkElement.addEventListener('load', () => {
|
||||
themeLink.remove();
|
||||
cloneLinkElement.setAttribute('id', id);
|
||||
});
|
||||
}
|
||||
|
||||
changeScale(value: number) {
|
||||
document.documentElement.style.fontSize = `${value}px`;
|
||||
}
|
||||
changeScale(value: number) {
|
||||
document.documentElement.style.fontSize = `${value}px`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<div class="layout-topbar">
|
||||
|
||||
<a class="layout-topbar-logo pl-5 pr-3 flex gap-2" routerLink="">
|
||||
<a class="layout-topbar-logo pl-5 pr-24 flex gap-2" routerLink="">
|
||||
<svg class="w-[1.875rem] h-[1.875rem] half-title" viewBox="0 0 126 126" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
|
||||
<path d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z" fill="white"/>
|
||||
<path
|
||||
d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
|
||||
<path
|
||||
d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z"
|
||||
fill="white"/>
|
||||
</svg>
|
||||
<span class="flex items-center pt-2">
|
||||
<p>Book</p>
|
||||
@@ -12,18 +15,19 @@
|
||||
</a>
|
||||
|
||||
<p-button
|
||||
class="layout-menu-button"
|
||||
#menubutton
|
||||
icon="pi pi-bars"
|
||||
[rounded]="true"
|
||||
[outlined]="true"
|
||||
(click)="toggleMenu()"
|
||||
[ngClass]="{ 'rotate-right': !isMenuVisible, 'rotate-left': isMenuVisible }"
|
||||
>
|
||||
>
|
||||
</p-button>
|
||||
|
||||
<div class="topbar-right-items">
|
||||
<app-book-searcher></app-book-searcher>
|
||||
<ul class="topbar-items pl-6">
|
||||
<div class="flex items-center w-full gap-4">
|
||||
<app-book-searcher class="md:block flex-grow"></app-book-searcher>
|
||||
<ul class="topbar-items hidden md:flex items-center gap-3 ml-auto pl-4">
|
||||
<div class="flex gap-6">
|
||||
@if (userService.userState$ | async; as userData) {
|
||||
<li>
|
||||
@@ -53,12 +57,12 @@
|
||||
<p-divider layout="vertical"/>
|
||||
<li class="relative" (mouseenter)="onHover(true)" (mouseleave)="onHover(false)">
|
||||
<button type="button" class="topbar-item"
|
||||
enterActiveClass="animate-scalein"
|
||||
enterFromClass="hidden"
|
||||
leaveActiveClass="animate-fadeout"
|
||||
leaveToClass="hidden"
|
||||
pStyleClass="@next"
|
||||
[hideOnOutsideClick]="true">
|
||||
enterActiveClass="animate-scalein"
|
||||
enterFromClass="hidden"
|
||||
leaveActiveClass="animate-fadeout"
|
||||
leaveToClass="hidden"
|
||||
pStyleClass="@next"
|
||||
[hideOnOutsideClick]="true">
|
||||
<i class="pi pi-wave-pulse" style="font-size: 1.5rem" [class.pulsating]="eventHighlight" [class.highlight]="eventHighlight"></i>
|
||||
</button>
|
||||
@if (isHovered) {
|
||||
@@ -79,12 +83,12 @@
|
||||
</li>
|
||||
<li class="relative">
|
||||
<button type="button"
|
||||
class="topbar-item config-item"
|
||||
enterActiveClass="animate-scalein"
|
||||
enterFromClass="hidden"
|
||||
leaveActiveClass="animate-fadeout"
|
||||
leaveToClass="hidden"
|
||||
pStyleClass="@next" [hideOnOutsideClick]="true">
|
||||
class="topbar-item config-item"
|
||||
enterActiveClass="animate-scalein"
|
||||
enterFromClass="hidden"
|
||||
leaveActiveClass="animate-fadeout"
|
||||
leaveToClass="hidden"
|
||||
pStyleClass="@next" [hideOnOutsideClick]="true">
|
||||
<i class="pi pi-palette"></i>
|
||||
</button>
|
||||
<app-theme-configurator></app-theme-configurator>
|
||||
@@ -104,5 +108,75 @@
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
<!-- Trigger Button -->
|
||||
<div class="md:hidden relative ml-auto">
|
||||
<p-button
|
||||
icon="pi pi-ellipsis-v"
|
||||
outlined
|
||||
(click)="mobileMenu.toggle($event)"
|
||||
/>
|
||||
<p-popover #mobileMenu>
|
||||
<ul class="flex flex-col gap-1 w-48">
|
||||
@if (userService.userState$ | async; as userData) {
|
||||
@if (userData.permissions.canManipulateLibrary || userData.permissions.admin) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="openLibraryCreatorDialog(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-plus-circle text-surface-700 dark:text-surface-100"></i>
|
||||
Create Library
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="openFileUploadDialog(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-upload text-surface-700 dark:text-surface-100"></i>
|
||||
Upload Book
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="navigateToSettings(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-cog text-surface-700 dark:text-surface-100"></i>
|
||||
Settings
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="openGithubSupportDialog(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-thumbs-up text-surface-700 dark:text-surface-100"></i>
|
||||
Support BookLore
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="openUserProfileDialog(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-user text-surface-700 dark:text-surface-100"></i>
|
||||
Profile
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="logout(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-sign-out text-surface-700 dark:text-surface-100"></i>
|
||||
Logout
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</p-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {AuthService} from '../../../core/service/auth.service';
|
||||
import {UserService} from '../../../settings/user-management/user.service';
|
||||
import {UserProfileDialogComponent} from '../../../settings/global-preferences/user-profile-dialog/user-profile-dialog.component';
|
||||
import {GithubSupportDialog} from '../../../github-support-dialog/github-support-dialog';
|
||||
import {Popover} from 'primeng/popover';
|
||||
|
||||
@Component({
|
||||
selector: 'app-topbar',
|
||||
@@ -37,8 +38,9 @@ import {GithubSupportDialog} from '../../../github-support-dialog/github-support
|
||||
NgClass,
|
||||
Divider,
|
||||
LiveNotificationBoxComponent,
|
||||
AsyncPipe
|
||||
],
|
||||
AsyncPipe,
|
||||
Popover
|
||||
],
|
||||
})
|
||||
export class AppTopBarComponent implements OnDestroy {
|
||||
items!: MenuItem[];
|
||||
@@ -62,6 +64,7 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
|
||||
isMenuVisible: boolean = true;
|
||||
isHovered: boolean = false;
|
||||
mobileActionsVisible!: boolean;
|
||||
|
||||
onHover(hovered: boolean): void {
|
||||
this.isHovered = hovered;
|
||||
|
||||
@@ -126,7 +126,8 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Reuse original filename in path:</strong><code class="text-gray-400"> {{ '{authors}/{series}/{currentFilename}' }}</code></p>
|
||||
<p class="mb-1"><strong class="text-gray-300">Reuse original filename in path:</strong><code class="text-gray-400"> {{ '{authors}/{series}/{currentFilename}' }}</code>
|
||||
</p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> J.K. Rowling/Harry Potter/harry1_original.epub</code></p>
|
||||
</div>
|
||||
|
||||
@@ -230,6 +231,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-[auto,1fr] pl-6 gap-y-4 gap-x-4 items-center">
|
||||
<p class="text-lg pb-4 ">Max File Upload Size:
|
||||
<p class="text-lg pb-4">Max File Upload Size:
|
||||
<i class="pi pi-info-circle text-sky-600"
|
||||
pTooltip="Defines the maximum allowed size (in MB) for each uploaded file. Applies to EPUB, PDF, CBZ, CBR, and CB7 formats."
|
||||
tooltipPosition="right"
|
||||
@@ -93,7 +93,7 @@
|
||||
type="text"
|
||||
pInputText
|
||||
[(ngModel)]="maxFileUploadSizeInMb"
|
||||
class="pr-10 w-full"
|
||||
class="pr-10 w-full min-w-24"
|
||||
placeholder="Max size"/>
|
||||
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 text-sm pointer-events-none">MB</span>
|
||||
</div>
|
||||
@@ -118,7 +118,7 @@
|
||||
type="text"
|
||||
pInputText
|
||||
[(ngModel)]="cbxCacheValue"
|
||||
class="pr-10 w-full"
|
||||
class="pr-10 w-full min-w-24"
|
||||
placeholder="Cache size"/>
|
||||
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 text-sm pointer-events-none">MB</span>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<p class="text-lg py-1">OPDS Endpoint:</p>
|
||||
<div class="flex items-center gap-4">
|
||||
<input
|
||||
class="min-w-[500px] max-w-[800px]"
|
||||
class="min-w-[600px] max-w-[600px]"
|
||||
type="text"
|
||||
pInputText
|
||||
[value]="opdsEndpoint"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="w-full h-[calc(100vh-11.65rem)] overflow-y-auto border rounded-lg enclosing-container">
|
||||
|
||||
<div class="p-4 pt-6">
|
||||
<div class="p-4 pt-6 min-w-[50rem]">
|
||||
<p class="text-lg pb-4 pt-2">Metadata Persistence:</p>
|
||||
|
||||
<div class="flex flex-col gap-4 pl-6">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h2 class="text-lg flex items-center gap-2">
|
||||
Current Booklore Users
|
||||
</h2>
|
||||
<p-button outlined="true" label="Create BookLore User" icon="pi pi-plus" (onClick)="openCreateUserDialog()"></p-button>
|
||||
<p-button outlined="true" label="Create User" icon="pi pi-plus" (onClick)="openCreateUserDialog()"></p-button>
|
||||
</div>
|
||||
|
||||
<p-table [value]="users" class="mt-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@if ((libraryState$ | async)?.libraries; as libraries) {
|
||||
<div
|
||||
class="flex flex-col gap-10 p-4 items-center justify-center min-w-[500px] max-w-[700px] w-full">
|
||||
class="flex flex-col gap-10 p-4 items-center justify-center w-full max-w-[700px]">
|
||||
<p-select
|
||||
[options]="libraries"
|
||||
optionLabel="name"
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="flex flex-col gap-8 px-4">
|
||||
@if (files?.length > 0) {
|
||||
<div>
|
||||
<div class="max-h-96 overflow-y-auto pr-2">
|
||||
<div class="max-h-96 max-w-[22rem] md:max-w-none overflow-y-auto pr-2">
|
||||
<div class="flex flex-wrap">
|
||||
@for (uploadFile of this.files; track uploadFile; let i = $index) {
|
||||
<div class="flex justify-between items-center w-full gap-4">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="h-[50rem] flex flex-col">
|
||||
<div class="h-[50rem] md:h-[50rem] md:w-[50rem] w-[25rem] flex flex-col">
|
||||
<div class="mb-6 flex items-center gap-4">
|
||||
<input
|
||||
id="disabled-input"
|
||||
@@ -7,14 +7,14 @@
|
||||
[value]="selectedProductName"
|
||||
[disabled]="true"
|
||||
class="w-full p-2 text-base"
|
||||
/>
|
||||
<p-button label="Select" severity="success" class="w-[100px]" (click)="onSelect()"></p-button>
|
||||
/>
|
||||
<p-button label="Select" severity="success" (click)="onSelect()"></p-button>
|
||||
</div>
|
||||
<p-table [value]="paths" [tableStyle]="{ 'min-width': '50rem' }" class="flex-grow overflow-y-auto">
|
||||
<p-table [value]="paths" styleClass="responsive-table" class="flex-grow overflow-y-auto">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th class="w-[10%]">Type</th>
|
||||
<th class="w-[90%]">Name</th>
|
||||
<th class="w-[1%]"></th>
|
||||
<th class="w-[99%]">Name</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.responsive-table {
|
||||
min-width: 50rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
min-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +1,102 @@
|
||||
@use "variables";
|
||||
|
||||
@media screen and (min-width: 1960px) {
|
||||
.layout-main, .landing-wrapper {
|
||||
width: 100%;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
.layout-main {
|
||||
width: 100%;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.layout-wrapper {
|
||||
&.layout-overlay {
|
||||
.layout-main-container {
|
||||
margin-left: 0;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.layout-wrapper {
|
||||
&.layout-overlay {
|
||||
.layout-main-container {
|
||||
margin-left: 0;
|
||||
padding: 5rem 1.1rem 2rem 3.6rem;
|
||||
}
|
||||
|
||||
.layout-sidebar {
|
||||
transform: translateX(-100%);
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.layout-sidebar {
|
||||
transform: translateX(-100%);
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
&.layout-overlay-active {
|
||||
.layout-sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.layout-static {
|
||||
.layout-main-container {
|
||||
padding-left: 255px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&.layout-static-inactive {
|
||||
.layout-sidebar {
|
||||
transform: translateX(-100%);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.layout-main-container {
|
||||
margin-left: 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: none;
|
||||
&.layout-overlay-active {
|
||||
.layout-sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.layout-static {
|
||||
.layout-main-container {
|
||||
padding-left: 255px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&.layout-static-inactive {
|
||||
.layout-sidebar {
|
||||
transform: translateX(-100%);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.layout-main-container {
|
||||
margin-left: 0;
|
||||
padding: 5rem 1rem 2rem 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.blocked-scroll {
|
||||
overflow: hidden;
|
||||
.blocked-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-wrapper {
|
||||
.layout-main-container {
|
||||
padding: 4.75rem 0.75rem 0.75rem 0.75rem;
|
||||
}
|
||||
|
||||
.layout-wrapper {
|
||||
.layout-main-container {
|
||||
margin-left: 0;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.layout-sidebar {
|
||||
transform: translateX(-100%);
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 998;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--maskbg);
|
||||
}
|
||||
|
||||
&.layout-mobile-active {
|
||||
.layout-sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: block;
|
||||
animation: fadein variables.$transitionDuration;
|
||||
}
|
||||
}
|
||||
.layout-sidebar {
|
||||
transform: translateX(-100%);
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 998;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&.layout-mobile-active {
|
||||
.layout-sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.layout-mask {
|
||||
display: block;
|
||||
animation: fadein variables.$transitionDuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,159 +1,36 @@
|
||||
@use "mixins";
|
||||
@use "variables";
|
||||
|
||||
.top-bar-app-theme-switcher {
|
||||
margin-left: 1.25rem;
|
||||
}
|
||||
|
||||
.layout-topbar {
|
||||
position: fixed;
|
||||
height: 3.85rem;
|
||||
z-index: 997;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding: 0 1.75rem;
|
||||
background-color: var(--card-background);
|
||||
transition: left variables.$transitionDuration;
|
||||
position: fixed;
|
||||
height: 3.85rem;
|
||||
z-index: 997;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
background-color: var(--card-background);
|
||||
transition: left variables.$transitionDuration;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0px 3px 5px rgba(0, 0, 0, .02), 0px 0px 2px rgba(0, 0, 0, .05), 0px 1px 4px rgba(0, 0, 0, .08);
|
||||
|
||||
.layout-topbar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0px 3px 5px rgba(0,0,0,.02), 0px 0px 2px rgba(0,0,0,.05), 0px 1px 4px rgba(0,0,0,.08);
|
||||
|
||||
.layout-topbar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--surface-900);
|
||||
font-size: 1.7rem;
|
||||
font-weight: 500;
|
||||
width: 265px;
|
||||
border-radius: 12px;
|
||||
|
||||
img {
|
||||
height: 2.5rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include mixins.focused();
|
||||
}
|
||||
}
|
||||
|
||||
.layout-topbar-button {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
color: var(--text-color-secondary);
|
||||
border-radius: 50%;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
cursor: pointer;
|
||||
transition: background-color variables.$transitionDuration;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include mixins.focused();
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 1rem;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-menu-button {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.layout-topbar-menu-button {
|
||||
display: none;
|
||||
|
||||
i {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-topbar-menu {
|
||||
margin: 0 0 0 auto;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.layout-topbar-button {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
font-size: 1.7rem;
|
||||
font-weight: 500;
|
||||
width: 265px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.layout-topbar {
|
||||
justify-content: space-between;
|
||||
|
||||
.layout-topbar-logo {
|
||||
width: auto;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.layout-menu-button {
|
||||
margin-left: 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.layout-topbar-menu-button {
|
||||
display: inline-flex;
|
||||
margin-left: 0;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.layout-topbar-menu {
|
||||
margin-left: 0;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
background-color: var(--surface-overlay);
|
||||
box-shadow: 0px 3px 5px rgba(0,0,0,.02), 0px 0px 2px rgba(0,0,0,.05), 0px 1px 4px rgba(0,0,0,.08);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
right: 2rem;
|
||||
top: 5rem;
|
||||
min-width: 15rem;
|
||||
display: none;
|
||||
-webkit-animation: scalein 0.15s linear;
|
||||
animation: scalein 0.15s linear;
|
||||
|
||||
&.layout-topbar-menu-mobile-active {
|
||||
display: block
|
||||
}
|
||||
|
||||
.layout-topbar-button {
|
||||
margin-left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
|
||||
i {
|
||||
font-size: 1rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: medium;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout-topbar {
|
||||
.layout-topbar-logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-items {
|
||||
@@ -272,12 +149,6 @@
|
||||
|
||||
}
|
||||
|
||||
.topbar-right-items {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.rotate-left {
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
|
||||
Reference in New Issue
Block a user