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:
WorldTeacher
2025-12-16 03:24:01 +01:00
committed by GitHub
parent 83f5e3a31d
commit a4a94b731a
16 changed files with 427 additions and 7 deletions

View File

@@ -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">

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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}`);
}