mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-01-08 12:00:33 -06:00
Add "OPDS Server Enabled" button to OPDS v2 settings
This commit is contained in:
committed by
Aditya Chandel
parent
cdd9b06b1d
commit
47e24e2241
@@ -125,7 +125,6 @@ export interface AppSettings {
|
||||
export enum AppSettingKey {
|
||||
QUICK_BOOK_MATCH = 'QUICK_BOOK_MATCH',
|
||||
AUTO_BOOK_SEARCH = 'AUTO_BOOK_SEARCH',
|
||||
COVER_IMAGE_RESOLUTION = 'COVER_IMAGE_RESOLUTION',
|
||||
SIMILAR_BOOK_RECOMMENDATION = 'SIMILAR_BOOK_RECOMMENDATION',
|
||||
UPLOAD_FILE_PATTERN = 'UPLOAD_FILE_PATTERN',
|
||||
OPDS_SERVER_ENABLED = 'OPDS_SERVER_ENABLED',
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--p-content-background);
|
||||
padding: 1rem 2rem 2rem 2rem;
|
||||
padding: 1rem 1.5rem 1.5rem 1.5rem;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
display: flex;
|
||||
@@ -107,7 +107,7 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 2rem;
|
||||
padding: 0.75rem 0;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@@ -117,7 +117,7 @@
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 0;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="settings-header">
|
||||
<h2 class="settings-title">
|
||||
<i class="pi pi-server"></i>
|
||||
OPDS Settings
|
||||
OPDS Settings v2
|
||||
</h2>
|
||||
<p class="settings-description">
|
||||
Manage your OPDS credentials and control how your book collection is shared with reading apps.
|
||||
@@ -12,134 +12,177 @@
|
||||
|
||||
@if (hasPermission) {
|
||||
<div class="settings-content">
|
||||
<div class="endpoint-section">
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-link"></i>
|
||||
OPDS Endpoint
|
||||
</h3>
|
||||
<div class="endpoint-form">
|
||||
<div class="endpoint-field">
|
||||
<input
|
||||
id="endpoint-url"
|
||||
fluid
|
||||
class="endpoint-input"
|
||||
type="text"
|
||||
pInputText
|
||||
[value]="opdsEndpoint"
|
||||
readonly/>
|
||||
<p-button
|
||||
icon="pi pi-copy"
|
||||
severity="info"
|
||||
outlined
|
||||
size="small"
|
||||
(onClick)="copyEndpoint()">
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="users-section">
|
||||
<div class="preferences-section">
|
||||
<div class="section-header">
|
||||
<div class="section-title-group">
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-power-off"></i>
|
||||
Server Control
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">OPDS Server Enabled</label>
|
||||
<p-toggleswitch
|
||||
[(ngModel)]="opdsEnabled"
|
||||
(onChange)="toggleOpdsServer()">
|
||||
</p-toggleswitch>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
Enable or disable the OPDS server to control access to your book collection through reading apps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (opdsEnabled) {
|
||||
<div class="preferences-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-users"></i>
|
||||
OPDS Users
|
||||
<i class="pi pi-link"></i>
|
||||
OPDS Endpoint
|
||||
</h3>
|
||||
<p-button
|
||||
icon="pi pi-plus"
|
||||
label="Add User"
|
||||
severity="success"
|
||||
size="small"
|
||||
(onClick)="showCreateUserDialog = true">
|
||||
</p-button>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Endpoint URL</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="endpoint-url"
|
||||
class="endpoint-input"
|
||||
fluid
|
||||
type="text"
|
||||
pInputText
|
||||
[value]="opdsEndpoint"
|
||||
readonly/>
|
||||
<p-button
|
||||
icon="pi pi-copy"
|
||||
severity="info"
|
||||
outlined
|
||||
size="small"
|
||||
(onClick)="copyEndpoint()">
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
Use this URL to connect your reading apps to your OPDS catalog.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<p-table
|
||||
[value]="users"
|
||||
[paginator]="users.length > 10"
|
||||
[rows]="10"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} users">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th>
|
||||
<div class="header-content">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>Username</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="header-content">
|
||||
<i class="pi pi-key"></i>
|
||||
<span>Password</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="actions-header">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-cog"></i>
|
||||
<span>Actions</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-user let-rowIndex="rowIndex">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">
|
||||
{{ user.username.charAt(0).toUpperCase() }}
|
||||
<div class="preferences-section">
|
||||
<div class="section-header">
|
||||
<div class="section-header-content">
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-users"></i>
|
||||
OPDS Users
|
||||
</h3>
|
||||
<p-button
|
||||
icon="pi pi-plus"
|
||||
label="Add User"
|
||||
severity="success"
|
||||
size="small"
|
||||
(onClick)="showCreateUserDialog = true">
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<p-table
|
||||
[value]="users"
|
||||
[paginator]="users.length > 10"
|
||||
[rows]="10"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} users">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th>
|
||||
<div class="header-content">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>Username</span>
|
||||
</div>
|
||||
<span class="username">{{ user.username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<p-password
|
||||
fluid
|
||||
class="w-32 md:w-56"
|
||||
[(ngModel)]="dummyPassword"
|
||||
[feedback]="false"
|
||||
</th>
|
||||
<th>
|
||||
<div class="header-content">
|
||||
<i class="pi pi-key"></i>
|
||||
<span>Password</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="actions-header">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-cog"></i>
|
||||
<span>Actions</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-user let-rowIndex="rowIndex">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">
|
||||
{{ user.username.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<span class="username">{{ user.username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<p-password
|
||||
fluid
|
||||
class="w-32 md:w-56"
|
||||
[(ngModel)]="dummyPassword"
|
||||
[feedback]="false"
|
||||
size="small"
|
||||
[disabled]="true"
|
||||
[toggleMask]="false">
|
||||
</p-password>
|
||||
<i
|
||||
class="pi pi-info-circle text-gray-400"
|
||||
pTooltip="Passwords are hidden for security reasons. To change, delete the user and create a new one with a new password."
|
||||
tooltipPosition="right"
|
||||
style="cursor: pointer;">
|
||||
</i>
|
||||
</div>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<p-button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
size="small"
|
||||
[disabled]="true"
|
||||
[toggleMask]="false">
|
||||
</p-password>
|
||||
<i
|
||||
class="pi pi-info-circle text-gray-400"
|
||||
pTooltip="Passwords are hidden for security reasons. To change, delete the user and create a new one with a new password."
|
||||
tooltipPosition="right"
|
||||
style="cursor: pointer;">
|
||||
</i>
|
||||
</div>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<p-button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="confirmDelete(user)"
|
||||
pTooltip="Delete user">
|
||||
</p-button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<div class="empty-message">
|
||||
<i class="pi pi-users"></i>
|
||||
<p class="empty-title">No users found</p>
|
||||
<p class="empty-subtitle">Create your first OPDS user to get started</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
[outlined]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="confirmDelete(user)"
|
||||
pTooltip="Delete user">
|
||||
</p-button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<div class="empty-message">
|
||||
<i class="pi pi-users"></i>
|
||||
<p class="empty-title">No users found</p>
|
||||
<p class="empty-subtitle">Create your first OPDS user to get started</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<p-dialog
|
||||
|
||||
@@ -16,6 +16,179 @@
|
||||
background: var(--p-content-background);
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--p-text-color);
|
||||
margin: 0 0 0.75rem 0;
|
||||
|
||||
.pi {
|
||||
color: var(--p-primary-color);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-description {
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.preferences-section {
|
||||
@media (min-width: 768px) {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
margin: 0;
|
||||
|
||||
.pi {
|
||||
color: var(--p-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--p-content-background);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 2rem;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.setting-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.setting-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.setting-label {
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
|
||||
.pi {
|
||||
color: var(--p-primary-color);
|
||||
margin-top: 0.125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.endpoint-input {
|
||||
width: 100%;
|
||||
min-width: 25rem;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--p-content-background);
|
||||
}
|
||||
|
||||
.p-datatable th .header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -28,19 +201,6 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--p-content-background);
|
||||
}
|
||||
|
||||
.p-datatable {
|
||||
.p-datatable-table {
|
||||
border-collapse: separate;
|
||||
@@ -73,6 +233,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@@ -90,17 +256,6 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.password-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.password-hidden, .password-visible {
|
||||
font-family: monospace;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -128,9 +283,6 @@
|
||||
}
|
||||
|
||||
.user-dialog {
|
||||
.p-dialog-header {
|
||||
}
|
||||
|
||||
.p-dialog-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
@@ -180,88 +332,6 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--p-text-color);
|
||||
margin: 0 0 0.75rem 0;
|
||||
|
||||
.pi {
|
||||
color: var(--p-primary-color);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-description {
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.endpoint-section {
|
||||
background: var(--p-content-background);
|
||||
border-radius: 8px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 0 1rem 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
|
||||
.pi {
|
||||
color: var(--p-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.endpoint-form {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.endpoint-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.endpoint-input {
|
||||
min-width: 9rem;
|
||||
max-width: 50rem;
|
||||
}
|
||||
|
||||
.users-section {
|
||||
@media (min-width: 768px) {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.section-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.access-denied-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -10,10 +10,13 @@ import {FormsModule} from '@angular/forms';
|
||||
import {ConfirmDialog} from 'primeng/confirmdialog';
|
||||
import {ConfirmationService, MessageService} from 'primeng/api';
|
||||
import {OpdsUserV2, OpdsUserV2CreateRequest, OpdsV2Service} from './opds-v2.service';
|
||||
import {catchError, filter, takeUntil, tap} from 'rxjs/operators';
|
||||
import {catchError, filter, take, takeUntil, tap} from 'rxjs/operators';
|
||||
import {UserService} from '../user-management/user.service';
|
||||
import {of, Subject} from 'rxjs';
|
||||
import {of, pipe, Subject} from 'rxjs';
|
||||
import {Password} from 'primeng/password';
|
||||
import {ToggleSwitch} from 'primeng/toggleswitch';
|
||||
import {AppSettingsService} from '../../core/service/app-settings.service';
|
||||
import {AppSettingKey} from '../../core/model/app-settings.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-opds-settings-v2',
|
||||
@@ -26,7 +29,8 @@ import {Password} from 'primeng/password';
|
||||
FormsModule,
|
||||
ConfirmDialog,
|
||||
TableModule,
|
||||
Password
|
||||
Password,
|
||||
ToggleSwitch
|
||||
],
|
||||
providers: [ConfirmationService],
|
||||
templateUrl: './opds-settings-v2.html',
|
||||
@@ -35,11 +39,13 @@ import {Password} from 'primeng/password';
|
||||
export class OpdsSettingsV2 implements OnInit, OnDestroy {
|
||||
|
||||
opdsEndpoint = `${API_CONFIG.BASE_URL}/api/v2/opds/catalog`;
|
||||
opdsEnabled = false;
|
||||
|
||||
private opdsService = inject(OpdsV2Service);
|
||||
private confirmationService = inject(ConfirmationService);
|
||||
private messageService = inject(MessageService);
|
||||
private userService = inject(UserService);
|
||||
private appSettingsService = inject(AppSettingsService);
|
||||
|
||||
users: OpdsUserV2[] = [];
|
||||
loading = false;
|
||||
@@ -61,10 +67,26 @@ export class OpdsSettingsV2 implements OnInit, OnDestroy {
|
||||
this.hasPermission = !!(state.user?.permissions.canAccessOpds || state.user?.permissions.admin);
|
||||
}),
|
||||
filter(() => this.hasPermission),
|
||||
tap(() => this.loadUsers())
|
||||
tap(() => this.loadAppSettings())
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
private loadAppSettings(): void {
|
||||
this.appSettingsService.appSettings$
|
||||
.pipe(
|
||||
filter((settings): settings is NonNullable<typeof settings> => settings != null),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(settings => {
|
||||
this.opdsEnabled = settings.opdsServerEnabled ?? false;
|
||||
if (this.opdsEnabled) {
|
||||
this.loadUsers();
|
||||
} else {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private loadUsers(): void {
|
||||
this.opdsService.getUser().pipe(
|
||||
takeUntil(this.destroy$),
|
||||
@@ -135,6 +157,29 @@ export class OpdsSettingsV2 implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
toggleOpdsServer(): void {
|
||||
this.saveSetting(AppSettingKey.OPDS_SERVER_ENABLED, this.opdsEnabled);
|
||||
if (this.opdsEnabled) {
|
||||
this.loadUsers();
|
||||
} else {
|
||||
this.users = [];
|
||||
}
|
||||
}
|
||||
|
||||
private saveSetting(key: string, value: unknown): void {
|
||||
this.appSettingsService.saveSettings([{key, newValue: value}]).subscribe({
|
||||
next: () => {
|
||||
const successMessage = (value === true)
|
||||
? 'OPDS Server Enabled.'
|
||||
: 'OPDS Server Disabled.';
|
||||
this.showMessage('success', 'Settings Saved', successMessage);
|
||||
},
|
||||
error: () => {
|
||||
this.showMessage('error', 'Error', 'There was an error saving the settings.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private resetCreateUserDialog(): void {
|
||||
this.showCreateUserDialog = false;
|
||||
this.newUser = {username: '', password: ''};
|
||||
|
||||
@@ -2,16 +2,10 @@
|
||||
<div class="settings-header">
|
||||
<h2 class="settings-title">
|
||||
<i class="pi pi-server"></i>
|
||||
OPDS Settings (v1)
|
||||
OPDS Settings v1 (Legacy)
|
||||
</h2>
|
||||
<p class="settings-description">
|
||||
Legacy OPDS server settings for managing your book collection access.
|
||||
<i
|
||||
class="pi pi-info-circle text-sky-600 ml-1"
|
||||
pTooltip="OPDS allows your book collection to be accessed by compatible reading apps through your private server."
|
||||
tooltipPosition="right"
|
||||
style="cursor: pointer;">
|
||||
</i>
|
||||
</p>
|
||||
|
||||
<div class="deprecation-notice">
|
||||
@@ -19,7 +13,7 @@
|
||||
<div>
|
||||
<p class="notice-title">Deprecated:</p>
|
||||
<p class="notice-text">
|
||||
OPDS (v1) support will be removed in a future release.
|
||||
OPDS v1 support will be removed in a future release.
|
||||
Please migrate to <strong>OPDS v2</strong> for continued support and improvements.
|
||||
</p>
|
||||
</div>
|
||||
@@ -27,56 +21,79 @@
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<div class="server-section">
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-power-off"></i>
|
||||
Server Control
|
||||
</h3>
|
||||
<div class="server-control">
|
||||
<label class="control-label">OPDS Server Enabled:</label>
|
||||
<p-toggleswitch
|
||||
[(ngModel)]="opdsEnabled"
|
||||
(onChange)="toggleOpdsServer()">
|
||||
</p-toggleswitch>
|
||||
<div class="preferences-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-power-off"></i>
|
||||
Server Control
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">OPDS Server Enabled</label>
|
||||
<p-toggleswitch
|
||||
[(ngModel)]="opdsEnabled"
|
||||
(onChange)="toggleOpdsServer()">
|
||||
</p-toggleswitch>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
Enable or disable the OPDS server to control access to your book collection through reading apps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (opdsEnabled) {
|
||||
<div class="endpoint-section">
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-link"></i>
|
||||
OPDS Endpoint
|
||||
</h3>
|
||||
<div class="endpoint-form">
|
||||
<div class="endpoint-field">
|
||||
<input
|
||||
id="endpoint-url"
|
||||
fluid
|
||||
class="endpoint-input"
|
||||
type="text"
|
||||
pInputText
|
||||
[value]="opdsEndpoint"
|
||||
readonly/>
|
||||
<p-button
|
||||
icon="pi pi-copy"
|
||||
label="Copy"
|
||||
severity="info"
|
||||
outlined
|
||||
size="small"
|
||||
(onClick)="copyOpdsEndpoint()">
|
||||
</p-button>
|
||||
<div class="preferences-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-link"></i>
|
||||
OPDS Endpoint
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Endpoint URL</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="endpoint-url"
|
||||
class="endpoint-input"
|
||||
type="text"
|
||||
pInputText
|
||||
[value]="opdsEndpoint"
|
||||
readonly/>
|
||||
<p-button
|
||||
icon="pi pi-copy"
|
||||
severity="info"
|
||||
outlined
|
||||
size="small"
|
||||
(onClick)="copyOpdsEndpoint()">
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
Use this URL to connect your reading apps to your OPDS catalog.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="users-section">
|
||||
<div class="preferences-section">
|
||||
<div class="section-header">
|
||||
<div class="section-title-group">
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-users"></i>
|
||||
OPDS Users
|
||||
</h3>
|
||||
</div>
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-users"></i>
|
||||
OPDS Users
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
|
||||
@@ -82,15 +82,16 @@
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.server-section {
|
||||
background: var(--p-content-background);
|
||||
border-radius: 8px;
|
||||
|
||||
.preferences-section {
|
||||
@media (min-width: 768px) {
|
||||
padding: 0 1rem 0 1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -98,58 +99,110 @@
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
margin-bottom: 1rem;
|
||||
margin: 0;
|
||||
|
||||
.pi {
|
||||
color: var(--p-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.server-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-weight: 500;
|
||||
color: var(--p-text-color);
|
||||
}
|
||||
|
||||
.endpoint-section {
|
||||
background: var(--p-content-background);
|
||||
.settings-card {
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--p-content-background);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 0 1rem 0 1rem;
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 2rem;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.endpoint-form {
|
||||
margin-top: 1rem;
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.setting-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.setting-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.setting-label {
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
|
||||
.pi {
|
||||
color: var(--p-primary-color);
|
||||
margin-top: 0.125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.endpoint-field {
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.endpoint-input {
|
||||
min-width: 9rem;
|
||||
max-width: 50rem;
|
||||
}
|
||||
|
||||
.users-section {
|
||||
@media (min-width: 768px) {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
max-width: 30rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</p-tab>
|
||||
}
|
||||
<p-tab [value]="SettingsTab.OpdsV2">
|
||||
<i class="pi pi-globe"></i> OPDS V2
|
||||
<i class="pi pi-globe"></i> OPDS v2
|
||||
</p-tab>
|
||||
<p-tab [value]="SettingsTab.DeviceSettings">
|
||||
<i class="pi pi-mobile"></i> Devices
|
||||
|
||||
Reference in New Issue
Block a user