mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
feat(opds): allow user to set sorting for opds feed in settings (#1824)
* feat(opds): allow user to set sorting for opds feed in settings * patch(opds): re-add search normalization * patch(opds): add series to feedid determination --------- Co-authored-by: WorldTeacher <admin@theprivateserver.de>
This commit is contained in:
@@ -113,6 +113,12 @@
|
||||
<span>Username</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="header-content">
|
||||
<i class="pi pi-sort-alt"></i>
|
||||
<span>Sort Order</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="header-content">
|
||||
<i class="pi pi-key"></i>
|
||||
@@ -137,6 +143,47 @@
|
||||
<span class="username">{{ user.username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if (editingUserId === user.id) {
|
||||
<div class="flex items-center gap-2">
|
||||
<p-select
|
||||
[options]="sortOrderOptions"
|
||||
[(ngModel)]="editingSortOrder"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
[style]="{width: '180px'}"
|
||||
size="small"
|
||||
[appendTo]="'body'">
|
||||
</p-select>
|
||||
<p-button
|
||||
icon="pi pi-check"
|
||||
severity="success"
|
||||
size="small"
|
||||
[text]="true"
|
||||
(onClick)="saveSortOrder(user)">
|
||||
</p-button>
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
[text]="true"
|
||||
(onClick)="cancelEdit()">
|
||||
</p-button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="sort-order-badge">{{ getSortOrderLabel(user.sortOrder) }}</span>
|
||||
<p-button
|
||||
icon="pi pi-pencil"
|
||||
severity="info"
|
||||
size="small"
|
||||
[text]="true"
|
||||
(onClick)="startEdit(user)"
|
||||
pTooltip="Edit sort order">
|
||||
</p-button>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<p-password
|
||||
@@ -171,7 +218,7 @@
|
||||
</ng-template>
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<td colspan="4">
|
||||
<div class="empty-message">
|
||||
<i class="pi pi-users"></i>
|
||||
<p class="empty-title">No users found</p>
|
||||
@@ -211,6 +258,23 @@
|
||||
[(ngModel)]="newUser.password"
|
||||
placeholder="Enter password"/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="sortOrder">Default Sort Order</label>
|
||||
<p-select
|
||||
id="sortOrder"
|
||||
[options]="sortOrderOptions"
|
||||
[(ngModel)]="newUser.sortOrder"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
[style]="{width: '100%'}"
|
||||
placeholder="Select sort order"
|
||||
[appendTo]="'body'">
|
||||
</p-select>
|
||||
<small class="form-hint">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
This will determine how books appear in the OPDS feed for this user
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="dialog-actions">
|
||||
|
||||
@@ -264,6 +264,17 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sort-order-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
background: var(--p-primary-50);
|
||||
color: var(--p-primary-700);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -332,6 +343,22 @@
|
||||
color: var(--p-text-muted-color);
|
||||
}
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.375rem;
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
.pi {
|
||||
font-size: 0.625rem;
|
||||
margin-top: 0.125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {Dialog} from 'primeng/dialog';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {ConfirmDialog} from 'primeng/confirmdialog';
|
||||
import {ConfirmationService, MessageService} from 'primeng/api';
|
||||
import {OpdsService, OpdsUserV2, OpdsUserV2CreateRequest} from './opds.service';
|
||||
import {OpdsService, OpdsSortOrder, OpdsUserV2, OpdsUserV2CreateRequest} from './opds.service';
|
||||
import {catchError, filter, take, takeUntil, tap} from 'rxjs/operators';
|
||||
import {UserService} from '../user-management/user.service';
|
||||
import {of, Subject} from 'rxjs';
|
||||
@@ -18,6 +18,7 @@ import {ToggleSwitch} from 'primeng/toggleswitch';
|
||||
import {AppSettingsService} from '../../../shared/service/app-settings.service';
|
||||
import {AppSettingKey} from '../../../shared/model/app-settings.model';
|
||||
import {ExternalDocLinkComponent} from '../../../shared/components/external-doc-link/external-doc-link.component';
|
||||
import {Select} from 'primeng/select';
|
||||
|
||||
@Component({
|
||||
selector: 'app-opds-settings',
|
||||
@@ -32,7 +33,8 @@ import {ExternalDocLinkComponent} from '../../../shared/components/external-doc-
|
||||
TableModule,
|
||||
Password,
|
||||
ToggleSwitch,
|
||||
ExternalDocLinkComponent
|
||||
ExternalDocLinkComponent,
|
||||
Select
|
||||
],
|
||||
providers: [ConfirmationService],
|
||||
templateUrl: './opds-settings.html',
|
||||
@@ -52,13 +54,28 @@ export class OpdsSettings implements OnInit, OnDestroy {
|
||||
users: OpdsUserV2[] = [];
|
||||
loading = false;
|
||||
showCreateUserDialog = false;
|
||||
newUser: OpdsUserV2CreateRequest = {username: '', password: ''};
|
||||
newUser: OpdsUserV2CreateRequest = {username: '', password: '', sortOrder: 'RECENT'};
|
||||
passwordVisibility: boolean[] = [];
|
||||
hasPermission = false;
|
||||
|
||||
editingUserId: number | null = null;
|
||||
editingSortOrder: OpdsSortOrder | null = null;
|
||||
|
||||
private readonly destroy$ = new Subject<void>();
|
||||
dummyPassword: string = "***********************";
|
||||
|
||||
sortOrderOptions = [
|
||||
{ label: 'Recently Added', value: 'RECENT' as OpdsSortOrder },
|
||||
{ label: 'Title (A-Z)', value: 'TITLE_ASC' as OpdsSortOrder },
|
||||
{ label: 'Title (Z-A)', value: 'TITLE_DESC' as OpdsSortOrder },
|
||||
{ label: 'Author (A-Z)', value: 'AUTHOR_ASC' as OpdsSortOrder },
|
||||
{ label: 'Author (Z-A)', value: 'AUTHOR_DESC' as OpdsSortOrder },
|
||||
{ label: 'Series (A-Z)', value: 'SERIES_ASC' as OpdsSortOrder },
|
||||
{ label: 'Series (Z-A)', value: 'SERIES_DESC' as OpdsSortOrder },
|
||||
{ label: 'Rating (Low to High)', value: 'RATING_ASC' as OpdsSortOrder },
|
||||
{ label: 'Rating (High to Low)', value: 'RATING_DESC' as OpdsSortOrder }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
|
||||
@@ -189,13 +206,51 @@ export class OpdsSettings implements OnInit, OnDestroy {
|
||||
|
||||
private resetCreateUserDialog(): void {
|
||||
this.showCreateUserDialog = false;
|
||||
this.newUser = {username: '', password: ''};
|
||||
this.newUser = {username: '', password: '', sortOrder: 'RECENT'};
|
||||
}
|
||||
|
||||
private showMessage(severity: string, summary: string, detail: string): void {
|
||||
this.messageService.add({severity, summary, detail});
|
||||
}
|
||||
|
||||
getSortOrderLabel(sortOrder?: OpdsSortOrder): string {
|
||||
if (!sortOrder) return 'Recently Added';
|
||||
const option = this.sortOrderOptions.find(o => o.value === sortOrder);
|
||||
return option ? option.label : 'Recently Added';
|
||||
}
|
||||
|
||||
startEdit(user: OpdsUserV2): void {
|
||||
this.editingUserId = user.id;
|
||||
this.editingSortOrder = user.sortOrder || 'RECENT';
|
||||
}
|
||||
|
||||
cancelEdit(): void {
|
||||
this.editingUserId = null;
|
||||
this.editingSortOrder = null;
|
||||
}
|
||||
|
||||
saveSortOrder(user: OpdsUserV2): void {
|
||||
if (!this.editingSortOrder || !user.id) return;
|
||||
|
||||
this.opdsService.updateUser(user.id, this.editingSortOrder).pipe(
|
||||
takeUntil(this.destroy$),
|
||||
catchError(err => {
|
||||
console.error('Error updating sort order:', err);
|
||||
this.showMessage('error', 'Error', 'Failed to update sort order');
|
||||
return of(null);
|
||||
})
|
||||
).subscribe(updatedUser => {
|
||||
if (updatedUser) {
|
||||
const index = this.users.findIndex(u => u.id === user.id);
|
||||
if (index !== -1) {
|
||||
this.users[index] = updatedUser;
|
||||
}
|
||||
this.showMessage('success', 'Success', 'Sort order updated successfully');
|
||||
}
|
||||
this.cancelEdit();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
@@ -3,15 +3,23 @@ import {HttpClient} from '@angular/common/http';
|
||||
import {Observable} from 'rxjs';
|
||||
import {API_CONFIG} from '../../../core/config/api-config';
|
||||
|
||||
export type OpdsSortOrder = 'RECENT' | 'TITLE_ASC' | 'TITLE_DESC' | 'AUTHOR_ASC' | 'AUTHOR_DESC' | 'SERIES_ASC' | 'SERIES_DESC' | 'RATING_ASC' | 'RATING_DESC';
|
||||
|
||||
export interface OpdsUserV2CreateRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
sortOrder?: OpdsSortOrder;
|
||||
}
|
||||
|
||||
export interface OpdsUserV2UpdateRequest {
|
||||
sortOrder: OpdsSortOrder;
|
||||
}
|
||||
|
||||
export interface OpdsUserV2 {
|
||||
id: number;
|
||||
userId: number;
|
||||
username: string;
|
||||
sortOrder?: OpdsSortOrder;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
@@ -30,6 +38,10 @@ export class OpdsService {
|
||||
return this.http.post<OpdsUserV2>(this.baseUrl, user);
|
||||
}
|
||||
|
||||
updateUser(id: number, sortOrder: OpdsSortOrder): Observable<OpdsUserV2> {
|
||||
return this.http.patch<OpdsUserV2>(`${this.baseUrl}/${id}`, { sortOrder });
|
||||
}
|
||||
|
||||
deleteCredential(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.baseUrl}/${id}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user