Feature/bookmark feedback (#508)
* ImageService had a stream reset before writting out to array. Added logging statment for updating series metadata. Removed ConcurencyCheck due to bad update issue for CollectionTag. * Added a new screen which lets you quickly see all your bookmarks for a given user. * Built user bookmark page in user settings. Moved user settings to it's own lazy loaded module. Removed unneded debouncing from downloader and just used throttleTime instead. * Removed a not-yet implemented tab from series modal * Fixed a bug in clear bookmarks and adjusted icons within anchors to have proper styling
This commit is contained in:
parent
623e555633
commit
68bb5ed5a8
35 changed files with 554 additions and 270 deletions
|
|
@ -0,0 +1,42 @@
|
|||
<p *ngIf="series.length === 0 && !loadingBookmarks">
|
||||
There are no bookmarks. Try creating <a href="https://wiki.kavitareader.com/guides/webreader/bookmarks" target="_blank">one <i class="fa fa-external-link-alt" aria-hidden="true"></i></a>.
|
||||
</p>
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let series of series" class="list-group-item">
|
||||
<div>
|
||||
<h4>
|
||||
<a href="/library/{{series.libraryId}}/series/{{series.id}}" >{{series.name | titlecase}}</a>
|
||||
<span class="badge badge-secondary badge-pill">{{getBookmarkPages(series.id)}}</span>
|
||||
<div class="float-right">
|
||||
<button class="btn btn-danger mr-2 btn-sm" (click)="clearBookmarks(series)" [disabled]="clearingSeries[series.id]" placement="top" ngbTooltip="Clear Bookmarks" attr.aria-label="Clear Bookmarks">
|
||||
<ng-container *ngIf="clearingSeries[series.id]; else notClearing">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</ng-container>
|
||||
<ng-template #notClearing>
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</ng-template>
|
||||
</button>
|
||||
<button class="btn btn-secondary mr-2 btn-sm" (click)="downloadBookmarks(series)" [disabled]="downloadingSeries[series.id]" placement="top" ngbTooltip="Download Bookmarks" attr.aria-label="Download Bookmarks">
|
||||
<ng-container *ngIf="downloadingSeries[series.id]; else notDownloading">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="sr-only">Downloading...</span>
|
||||
</ng-container>
|
||||
<ng-template #notDownloading>
|
||||
<i class="fa fa-arrow-alt-circle-down" aria-hidden="true"></i>
|
||||
</ng-template>
|
||||
</button>
|
||||
<button class="btn btn-primary mr-2 btn-sm" (click)="viewBookmarks(series)" placement="top" ngbTooltip="View Bookmarks" attr.aria-label="View Bookmarks">
|
||||
<i class="fa fa-pen" title="View Bookmarks"></i>
|
||||
</button>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="loadingBookmarks" class="list-group-item">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take, takeWhile, finalize } from 'rxjs/operators';
|
||||
import { BookmarksModalComponent } from 'src/app/cards/_modals/bookmarks-modal/bookmarks-modal.component';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { PageBookmark } from 'src/app/_models/page-bookmark';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-bookmarks',
|
||||
templateUrl: './series-bookmarks.component.html',
|
||||
styleUrls: ['./series-bookmarks.component.scss']
|
||||
})
|
||||
export class SeriesBookmarksComponent implements OnInit {
|
||||
|
||||
bookmarks: Array<PageBookmark> = [];
|
||||
series: Array<Series> = [];
|
||||
loadingBookmarks: boolean = false;
|
||||
seriesIds: {[id: number]: number} = {};
|
||||
downloadingSeries: {[id: number]: boolean} = {};
|
||||
clearingSeries: {[id: number]: boolean} = {};
|
||||
|
||||
constructor(private readerService: ReaderService, private seriesService: SeriesService,
|
||||
private modalService: NgbModal, private downloadService: DownloadService, private toastr: ToastrService,
|
||||
private confirmService: ConfirmService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadBookmarks();
|
||||
}
|
||||
|
||||
loadBookmarks() {
|
||||
this.loadingBookmarks = true;
|
||||
this.readerService.getAllBookmarks().pipe(take(1)).subscribe(bookmarks => {
|
||||
this.bookmarks = bookmarks;
|
||||
this.seriesIds = {};
|
||||
this.bookmarks.forEach(bmk => {
|
||||
if (!this.seriesIds.hasOwnProperty(bmk.seriesId)) {
|
||||
this.seriesIds[bmk.seriesId] = 1;
|
||||
} else {
|
||||
this.seriesIds[bmk.seriesId] += 1;
|
||||
}
|
||||
this.downloadingSeries[bmk.seriesId] = false;
|
||||
this.clearingSeries[bmk.seriesId] = false;
|
||||
});
|
||||
|
||||
const ids = Object.keys(this.seriesIds).map(k => parseInt(k, 10));
|
||||
this.seriesService.getAllSeriesByIds(ids).subscribe(series => {
|
||||
this.series = series;
|
||||
this.loadingBookmarks = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
viewBookmarks(series: Series) {
|
||||
const bookmarkModalRef = this.modalService.open(BookmarksModalComponent, { scrollable: true, size: 'lg' });
|
||||
bookmarkModalRef.componentInstance.series = series;
|
||||
bookmarkModalRef.closed.pipe(take(1)).subscribe(() => {
|
||||
this.loadBookmarks();
|
||||
});
|
||||
}
|
||||
|
||||
async clearBookmarks(series: Series) {
|
||||
if (!await this.confirmService.confirm('Are you sure you want to clear all bookmarks for ' + series.name + '? This cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearingSeries[series.id] = true;
|
||||
this.readerService.clearBookmarks(series.id).subscribe(() => {
|
||||
const index = this.series.indexOf(series);
|
||||
if (index > -1) {
|
||||
this.series.splice(index, 1);
|
||||
}
|
||||
this.clearingSeries[series.id] = false;
|
||||
this.toastr.success(series.name + '\'s bookmarks have been removed');
|
||||
});
|
||||
}
|
||||
|
||||
getBookmarkPages(seriesId: number) {
|
||||
return this.seriesIds[seriesId];
|
||||
}
|
||||
|
||||
downloadBookmarks(series: Series) {
|
||||
this.downloadingSeries[series.id] = true;
|
||||
this.downloadService.downloadBookmarks(this.bookmarks.filter(bmk => bmk.seriesId === series.id)).pipe(
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
}),
|
||||
finalize(() => {
|
||||
this.downloadingSeries[series.id] = false;
|
||||
})).subscribe(() => {/* No Operation */});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
<div class="container">
|
||||
<h2>User Dashboard</h2>
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | titlecase }}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngIf="tab.fragment === ''">
|
||||
<p>
|
||||
These are global settings that are bound to your account.
|
||||
</p>
|
||||
|
||||
<ngb-accordion [closeOthers]="true" activeIds="site-panel">
|
||||
<ngb-panel id="site-panel" title="Site">
|
||||
<ng-template ngbPanelContent>
|
||||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||
<div class="form-group">
|
||||
<label id="site-dark-mode-label" aria-describedby="site-heading">Dark Mode</label>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="site-dark-mode" formControlName="siteDarkMode" class="custom-control-input" [value]="true" aria-labelledby="site-dark-mode-label">
|
||||
<label class="custom-control-label" for="site-dark-mode">True</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="site-not-dark-mode" formControlName="siteDarkMode" class="custom-control-input" [value]="false" aria-labelledby="site-dark-mode-label">
|
||||
<label class="custom-control-label" for="site-not-dark-mode">False</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="float-right mb-3">
|
||||
<button type="button" class="btn btn-secondary mr-2" (click)="resetForm()">Reset</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="save()" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
<ngb-panel id="reading-panel" title="Reading">
|
||||
<ng-template ngbPanelContent>
|
||||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||
<h3 id="manga-header">Manga</h3>
|
||||
<div class="form-group">
|
||||
<label for="settings-reading-direction">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #readingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="sr-only" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="settings-scaling-option">Scaling Options</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskBackupTooltip>How to scale the image to your screen.</ng-template>
|
||||
<span class="sr-only" id="settings-scaling-option-help">How to scale the image to your screen.</span>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
|
||||
<option *ngFor="let opt of scalingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="settings-pagesplit-option">Page Splitting</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #pageSplitOptionTooltip>How to split a full width image (ie both left and right images are combined)</ng-template>
|
||||
<span class="sr-only" id="settings-pagesplit-option-help">How to split a full width image (ie both left and right images are combined)</span>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
|
||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settings-readingmode-option">Reading Mode</label>
|
||||
<select class="form-control" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
|
||||
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label id="auto-close-label">Auto Close Menu</label>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="auto-close" formControlName="autoCloseMenu" class="custom-control-input" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="custom-control-label" for="auto-close">True</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="not-auto-close" formControlName="autoCloseMenu" class="custom-control-input" [value]="false" aria-labelledby="auto-close-label">
|
||||
<label class="custom-control-label" for="not-auto-close">False</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h3>Books</h3>
|
||||
<div class="form-group">
|
||||
<label id="dark-mode-label">Dark Mode</label>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="true" aria-labelledby="dark-mode-label">
|
||||
<label class="custom-control-label" for="dark-mode">True</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="not-dark-mode" formControlName="bookReaderDarkMode" class="custom-control-input" [value]="false" aria-labelledby="dark-mode-label">
|
||||
<label class="custom-control-label" for="not-dark-mode">False</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settings-book-reading-direction">Book Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReadingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="sr-only" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-control" aria-describedby="settings-reading-direction-help" formControlName="bookReaderReadingDirection">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label id="taptopaginate-label">Tap to Paginate</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #tapToPaginateOptionTooltip>Should the sides of the book reader screen allow tapping on it to move to prev/next page</ng-template>
|
||||
<span class="sr-only" id="settings-taptopaginate-option-help">Should the sides of the book reader screen allow tapping on it to move to prev/next page</span>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="custom-control-input" [value]="true" aria-labelledby="taptopaginate-label">
|
||||
<label class="custom-control-label" for="taptopaginate">True</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio custom-control-inline">
|
||||
<input type="radio" id="not-taptopaginate" formControlName="bookReaderTapToPaginate" class="custom-control-input" [value]="false" aria-labelledby="taptopaginate-label">
|
||||
<label class="custom-control-label" for="not-taptopaginate">False</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="settings-fontfamily-option">Font Family</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #fontFamilyOptionTooltip>Font familty to load up. Default will load the book's default font</ng-template>
|
||||
<span class="sr-only" id="settings-fontfamily-option-help">Font familty to load up. Default will load the book's default font</span>
|
||||
<select class="form-control" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
||||
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label id="font-size">Font Size</label>
|
||||
<ngx-slider [options]="bookReaderFontSizeOptions" formControlName="bookReaderFontSize" aria-labelledby="font-size"></ngx-slider>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Line Height</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLineHeightOptionTooltip>How much spacing between the lines of the book</ng-template>
|
||||
<span class="sr-only" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
|
||||
<ngx-slider [options]="bookReaderLineSpacingOptions" formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help"></ngx-slider>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Margin</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReaderMarginOptionTooltip>How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</ng-template>
|
||||
<span class="sr-only" id="settings-bookmargin-option-help">How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</span>
|
||||
<ngx-slider [options]="bookReaderMarginOptions" formControlName="bookReaderMargin" aria-describedby="bookmargin"></ngx-slider>
|
||||
</div>
|
||||
|
||||
<div class="float-right mb-3">
|
||||
<button type="button" class="btn btn-secondary mr-2" (click)="resetForm()">Reset</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="save()" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
|
||||
|
||||
<ngb-panel id="password-panel" title="Password">
|
||||
<ng-template ngbPanelContent>
|
||||
<p>Change your Password</p>
|
||||
<div class="alert alert-danger" role="alert" *ngIf="resetPasswordErrors.length > 0">
|
||||
<div *ngFor="let error of resetPasswordErrors">{{error}}</div>
|
||||
</div>
|
||||
<form [formGroup]="passwordChangeForm">
|
||||
<div class="form-group">
|
||||
<label for="new-password">New Password</label>
|
||||
<input class="form-control" type="password" id="new-password" formControlName="password" required>
|
||||
<div id="password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
||||
<div *ngIf="password?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirm Password</label>
|
||||
<input class="form-control" type="password" id="confirm-password" formControlName="confirmPassword" aria-describedby="password-validations" required>
|
||||
<div id="password-validations" class="invalid-feedback" *ngIf="passwordChangeForm.dirty || passwordChangeForm.touched">
|
||||
<div *ngIf="!passwordsMatch">
|
||||
Passwords must match
|
||||
</div>
|
||||
<div *ngIf="confirmPassword?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="float-right mb-3">
|
||||
<button type="button" class="btn btn-secondary mr-2" (click)="resetPasswordForm()">Reset</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="savePasswordForm()" [disabled]="!passwordChangeForm.valid || !(passwordChangeForm.dirty || passwordChangeForm.touched)">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === 'bookmarks'">
|
||||
<app-series-bookmarks></app-series-bookmarks>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.invalid-feedback {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { Options } from '@angular-slider/ngx-slider';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { BookService } from 'src/app/book-reader/book.service';
|
||||
import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences } from 'src/app/_models/preferences/preferences';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { NavService } from 'src/app/_services/nav.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-preferences',
|
||||
templateUrl: './user-preferences.component.html',
|
||||
styleUrls: ['./user-preferences.component.scss']
|
||||
})
|
||||
export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
|
||||
readingDirections = readingDirections;
|
||||
scalingOptions = scalingOptions;
|
||||
pageSplitOptions = pageSplitOptions;
|
||||
readingModes = readingModes;
|
||||
|
||||
settingsForm: FormGroup = new FormGroup({});
|
||||
passwordChangeForm: FormGroup = new FormGroup({});
|
||||
user: User | undefined = undefined;
|
||||
|
||||
passwordsMatch = false;
|
||||
resetPasswordErrors: string[] = [];
|
||||
|
||||
obserableHandles: Array<any> = [];
|
||||
|
||||
bookReaderLineSpacingOptions: Options = {
|
||||
floor: 100,
|
||||
ceil: 250,
|
||||
step: 10,
|
||||
};
|
||||
bookReaderMarginOptions: Options = {
|
||||
floor: 0,
|
||||
ceil: 30,
|
||||
step: 5,
|
||||
};
|
||||
bookReaderFontSizeOptions: Options = {
|
||||
floor: 50,
|
||||
ceil: 300,
|
||||
step: 10,
|
||||
};
|
||||
fontFamilies: Array<string> = [];
|
||||
|
||||
//tabs = ['Preferences', 'Bookmarks'];
|
||||
tabs: Array<{title: string, fragment: string}> = [
|
||||
{title: 'Preferences', fragment: ''},
|
||||
{title: 'Bookmarks', fragment: 'bookmarks'},
|
||||
];
|
||||
active = this.tabs[0];
|
||||
|
||||
constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService,
|
||||
private navService: NavService, private titleService: Title, private route: ActivatedRoute) {
|
||||
this.fontFamilies = this.bookService.getFontFamilies();
|
||||
|
||||
this.route.fragment.subscribe(frag => {
|
||||
const tab = this.tabs.filter(item => item.fragment === frag);
|
||||
if (tab.length > 0) {
|
||||
this.active = tab[0];
|
||||
} else {
|
||||
this.active = this.tabs[0]; // Default to first tab
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.titleService.setTitle('Kavita - User Preferences');
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe((user: User) => {
|
||||
if (user) {
|
||||
this.user = user;
|
||||
|
||||
if (this.fontFamilies.indexOf(this.user.preferences.bookReaderFontFamily) < 0) {
|
||||
this.user.preferences.bookReaderFontFamily = 'default';
|
||||
}
|
||||
|
||||
this.settingsForm.addControl('readingDirection', new FormControl(user.preferences.readingDirection, []));
|
||||
this.settingsForm.addControl('scalingOption', new FormControl(user.preferences.scalingOption, []));
|
||||
this.settingsForm.addControl('pageSplitOption', new FormControl(user.preferences.pageSplitOption, []));
|
||||
this.settingsForm.addControl('autoCloseMenu', new FormControl(user.preferences.autoCloseMenu, []));
|
||||
this.settingsForm.addControl('readerMode', new FormControl(user.preferences.readerMode, []));
|
||||
this.settingsForm.addControl('bookReaderDarkMode', new FormControl(user.preferences.bookReaderDarkMode, []));
|
||||
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(user.preferences.bookReaderFontFamily, []));
|
||||
this.settingsForm.addControl('bookReaderFontSize', new FormControl(user.preferences.bookReaderFontSize, []));
|
||||
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(user.preferences.bookReaderLineSpacing, []));
|
||||
this.settingsForm.addControl('bookReaderMargin', new FormControl(user.preferences.bookReaderMargin, []));
|
||||
this.settingsForm.addControl('bookReaderReadingDirection', new FormControl(user.preferences.bookReaderReadingDirection, []));
|
||||
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(!!user.preferences.siteDarkMode, []));
|
||||
|
||||
this.settingsForm.addControl('siteDarkMode', new FormControl(!!user.preferences.siteDarkMode, []));
|
||||
}
|
||||
});
|
||||
|
||||
this.passwordChangeForm.addControl('password', new FormControl('', [Validators.required]));
|
||||
this.passwordChangeForm.addControl('confirmPassword', new FormControl('', [Validators.required]));
|
||||
|
||||
this.obserableHandles.push(this.passwordChangeForm.valueChanges.subscribe(() => {
|
||||
const values = this.passwordChangeForm.value;
|
||||
this.passwordsMatch = values.password === values.confirmPassword;
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.obserableHandles.forEach(o => o.unsubscribe());
|
||||
}
|
||||
|
||||
public get password() { return this.passwordChangeForm.get('password'); }
|
||||
public get confirmPassword() { return this.passwordChangeForm.get('confirmPassword'); }
|
||||
|
||||
resetForm() {
|
||||
if (this.user === undefined) { return; }
|
||||
this.settingsForm.get('readingDirection')?.setValue(this.user.preferences.readingDirection);
|
||||
this.settingsForm.get('scalingOption')?.setValue(this.user.preferences.scalingOption);
|
||||
this.settingsForm.get('autoCloseMenu')?.setValue(this.user.preferences.autoCloseMenu);
|
||||
this.settingsForm.get('readerMode')?.setValue(this.user.preferences.readerMode);
|
||||
this.settingsForm.get('pageSplitOption')?.setValue(this.user.preferences.pageSplitOption);
|
||||
this.settingsForm.get('bookReaderDarkMode')?.setValue(this.user.preferences.bookReaderDarkMode);
|
||||
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
|
||||
this.settingsForm.get('bookReaderFontSize')?.setValue(this.user.preferences.bookReaderFontSize);
|
||||
this.settingsForm.get('bookReaderLineSpacing')?.setValue(this.user.preferences.bookReaderLineSpacing);
|
||||
this.settingsForm.get('bookReaderMargin')?.setValue(this.user.preferences.bookReaderMargin);
|
||||
this.settingsForm.get('bookReaderTapToPaginate')?.setValue(this.user.preferences.bookReaderTapToPaginate);
|
||||
this.settingsForm.get('bookReaderReadingDirection')?.setValue(this.user.preferences.bookReaderReadingDirection);
|
||||
this.settingsForm.get('siteDarkMode')?.setValue(this.user.preferences.siteDarkMode);
|
||||
}
|
||||
|
||||
resetPasswordForm() {
|
||||
this.passwordChangeForm.get('password')?.setValue('');
|
||||
this.passwordChangeForm.get('confirmPassword')?.setValue('');
|
||||
this.resetPasswordErrors = [];
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.user === undefined) return;
|
||||
const modelSettings = this.settingsForm.value;
|
||||
const data: Preferences = {
|
||||
readingDirection: parseInt(modelSettings.readingDirection, 10),
|
||||
scalingOption: parseInt(modelSettings.scalingOption, 10),
|
||||
pageSplitOption: parseInt(modelSettings.pageSplitOption, 10),
|
||||
autoCloseMenu: modelSettings.autoCloseMenu,
|
||||
readerMode: parseInt(modelSettings.readerMode),
|
||||
bookReaderDarkMode: modelSettings.bookReaderDarkMode,
|
||||
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
||||
bookReaderLineSpacing: modelSettings.bookReaderLineSpacing,
|
||||
bookReaderFontSize: modelSettings.bookReaderFontSize,
|
||||
bookReaderMargin: modelSettings.bookReaderMargin,
|
||||
bookReaderTapToPaginate: modelSettings.bookReaderTapToPaginate,
|
||||
bookReaderReadingDirection: parseInt(modelSettings.bookReaderReadingDirection, 10),
|
||||
siteDarkMode: modelSettings.siteDarkMode
|
||||
};
|
||||
this.obserableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||
this.toastr.success('Server settings updated');
|
||||
if (this.user) {
|
||||
this.user.preferences = updatedPrefs;
|
||||
|
||||
this.navService.setDarkMode(this.user.preferences.siteDarkMode);
|
||||
}
|
||||
this.resetForm();
|
||||
}));
|
||||
}
|
||||
|
||||
savePasswordForm() {
|
||||
if (this.user === undefined) { return; }
|
||||
|
||||
const model = this.passwordChangeForm.value;
|
||||
this.resetPasswordErrors = [];
|
||||
this.obserableHandles.push(this.accountService.resetPassword(this.user?.username, model.confirmPassword).subscribe(() => {
|
||||
this.toastr.success('Password has been updated');
|
||||
this.resetPasswordForm();
|
||||
}, err => {
|
||||
this.resetPasswordErrors = err;
|
||||
}));
|
||||
}
|
||||
}
|
||||
22
UI/Web/src/app/user-settings/user-settings-routing.module.ts
Normal file
22
UI/Web/src/app/user-settings/user-settings-routing.module.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AuthGuard } from '../_guards/auth.guard';
|
||||
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{path: '**', component: UserPreferencesComponent, pathMatch: 'full'},
|
||||
{
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{path: '', component: UserPreferencesComponent, pathMatch: 'full'},
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes), ],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class UserSettingsRoutingModule { }
|
||||
27
UI/Web/src/app/user-settings/user-settings.module.ts
Normal file
27
UI/Web/src/app/user-settings/user-settings.module.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SeriesBookmarksComponent } from './series-bookmarks/series-bookmarks.component';
|
||||
import { UserPreferencesComponent } from './user-preferences/user-preferences.component';
|
||||
import { NgbAccordionModule, NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { NgxSliderModule } from '@angular-slider/ngx-slider';
|
||||
import { UserSettingsRoutingModule } from './user-settings-routing.module';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
SeriesBookmarksComponent,
|
||||
UserPreferencesComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
NgbAccordionModule,
|
||||
NgbNavModule,
|
||||
NgbTooltipModule,
|
||||
NgxSliderModule,
|
||||
UserSettingsRoutingModule,
|
||||
]
|
||||
})
|
||||
export class UserSettingsModule { }
|
||||
Loading…
Add table
Add a link
Reference in a new issue