Reader Fixes and Enhancements (#880)

* Don't show an exception when bookmarking doesn't have anything to change.

* Cleaned up the bookmark code a bit.

* Implemented fullscreen mode in the web reader. Refactored User Settings to move Password and 3rd Party Clients to a tab rather than accordion. Removed color filters for web reader.

* Implemented fullscreen mode into book reader

* Added some code for toggling fullscreen which re-renders the screen to ensure the fitting works optimially

* Fixed an issue where moving from FitToScreen -> Split (L/R) wouldn't render the screen correctly due to canvas not being reset.

* Fixed bad optimization and scaling when drawing fit to screen

* Removed left/right highlights on page direction change in favor for icons. Double arrow will dictate the page change.

* Reduced overlay auto close time to 3 seconds

* Updated the paginging direction overlay to use icons and colors. Added a blur effect on menus

* Removed debug flags
This commit is contained in:
Joseph Milazzo 2022-01-02 18:10:37 -07:00 committed by GitHub
parent ca5c67020e
commit 720c52f494
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1620 additions and 166 deletions

View File

@ -468,18 +468,15 @@ namespace API.Controllers
_unitOfWork.UserRepository.Update(user);
}
if (await _unitOfWork.CommitAsync())
{
return Ok();
}
await _unitOfWork.CommitAsync();
}
catch (Exception)
{
await _unitOfWork.RollbackAsync();
return BadRequest("Could not save bookmark");
}
return BadRequest("Could not save bookmark");
return Ok();
}
/// <summary>

View File

@ -169,7 +169,7 @@ namespace API.Controllers
_logger.LogInformation("Server Settings updated");
_taskScheduler.ScheduleTasks();
await _taskScheduler.ScheduleTasks();
return Ok(updateSettingsDto);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class FullscreenPref : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "FullscreenMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FullscreenMode",
table: "AppUserPreferences");
}
}
}

View File

@ -183,6 +183,9 @@ namespace API.Data.Migrations
b.Property<bool>("BookReaderTapToPaginate")
.HasColumnType("INTEGER");
b.Property<int>("FullscreenMode")
.HasColumnType("INTEGER");
b.Property<int>("PageSplitOption")
.HasColumnType("INTEGER");

View File

@ -514,7 +514,7 @@ public class SeriesRepository : ISeriesRepository
var retSeries = query.Where(s => s.AppUserId == userId
&& s.PagesRead > 0
&& s.PagesRead < s.Series.Pages)
.OrderByDescending(s => s.LastModified)
.OrderByDescending(s => s.LastModified) // TODO: This needs to be Chapter Created (Max)
.ThenByDescending(s => s.Series.LastModified)
.Select(s => s.Series)
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)

View File

@ -4,10 +4,19 @@ namespace API.Entities.Enums
{
public enum LibraryType
{
/// <summary>
/// Uses Manga regex for filename parsing
/// </summary>
[Description("Manga")]
Manga = 0,
/// <summary>
/// Uses Comic regex for filename parsing
/// </summary>
[Description("Comic")]
Comic = 1,
/// <summary>
/// Uses Manga regex for filename parsing also uses epub metadata
/// </summary>
[Description("Book")]
Book = 2,
}

View File

@ -1,3 +1,4 @@
import { PageSplitOption } from './page-split-option';
import { READER_MODE } from './reader-mode';
import { ReadingDirection } from './reading-direction';

View File

@ -193,4 +193,48 @@ export class ReaderService {
}
return params;
}
enterFullscreen(el: Element, callback?: VoidFunction) {
if (!document.fullscreenElement) {
if (el.requestFullscreen) {
el.requestFullscreen().then(() => {
if (callback) {
callback();
}
});
}
// else if (el.mozRequestFullScreen) {
// el.mozRequestFullScreen();
// } else if (el.webkitRequestFullscreen) {
// el.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
// } else if (el.msRequestFullscreen) {
// el.msRequestFullscreen();
// }
}
}
exitFullscreen(callback?: VoidFunction) {
if (document.exitFullscreen && this.checkFullscreenMode()) {
document.exitFullscreen().then(() => {
if (callback) {
callback();
}
});
}
// else if (document.msExitFullscreen) {
// document.msExitFullscreen();
// } else if (document.mozCancelFullScreen) {
// document.mozCancelFullScreen();
// } else if (document.webkitExitFullscreen) {
// document.webkitExitFullscreen();
// }
}
/**
*
* @returns If document is in fullscreen mode
*/
checkFullscreenMode() {
return document.fullscreenElement != null;
}
}

View File

@ -1,4 +1,4 @@
<div class="container-flex {{darkMode ? 'dark-mode' : ''}}">
<div class="container-flex {{darkMode ? 'dark-mode' : ''}}" #reader>
<div class="fixed-top" #stickyTop>
<a class="sr-only sr-only-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
@ -57,6 +57,17 @@
<span class="sr-only" id="tap-pagination-help">The ability to click the sides of the page to page left and right</span>
<button (click)="toggleClickToPaginate()" class="btn btn-icon" aria-labelledby="tap-pagination"><i class="fa fa-arrows-alt-h {{clickToPaginate ? 'icon-primary-color' : ''}}" aria-hidden="true"></i><span *ngIf="darkMode">&nbsp;{{clickToPaginate ? 'On' : 'Off'}}</span></button>
</div>
<div class="controls">
<label id="fullscreen">Fullscreen&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="fullscreenTooltip" role="button" tabindex="0" aria-describedby="fullscreen-help"></i></label>
<ng-template #fullscreenTooltip>Put reader in fullscreen mode</ng-template>
<span class="sr-only" id="fullscreen-help">
<ng-container [ngTemplateOutlet]="fullscreenTooltip"></ng-container>
</span>
<button (click)="toggleFullscreen()" class="btn btn-icon" aria-labelledby="fullscreen">
<i class="fa {{this.isFullscreen ? 'fa-compress-alt' : 'fa-expand-alt'}} {{isFullscreen ? 'icon-primary-color' : ''}}" aria-hidden="true"></i>
<span *ngIf="darkMode">&nbsp;{{isFullscreen ? 'Exit' : 'Enter'}}</span>
</button>
</div>
<div class="row no-gutters justify-content-between">
<button (click)="resetSettings()" class="btn btn-primary col">Reset to Defaults</button>
</div>

View File

@ -111,6 +111,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('readingHtml', {static: false}) readingHtml!: ElementRef<HTMLDivElement>;
@ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('reader', {static: true}) reader!: ElementRef;
/**
* Next Chapter Id. This is not garunteed to be a valid ChapterId. Prefetched on page load (non-blocking).
@ -185,6 +186,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/
originalBodyColor: string | undefined;
/**
* If the web browser is in fullscreen mode
*/
isFullscreen: boolean = false;
darkModeStyles = `
*:not(input), *:not(select), *:not(code), *:not(:link), *:not(.ngx-toastr) {
color: #dcdcdc !important;
@ -365,6 +371,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.clickToPaginateVisualOverlayTimeout2 = undefined;
}
this.readerService.exitFullscreen();
this.onDestroy.next();
this.onDestroy.complete();
}
@ -1014,4 +1022,17 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
}
toggleFullscreen() {
this.isFullscreen = this.readerService.checkFullscreenMode();
if (this.isFullscreen) {
this.readerService.exitFullscreen(() => {
this.isFullscreen = false;
});
} else {
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
this.isFullscreen = true;
});
}
}
}

View File

@ -15,8 +15,3 @@ export enum PAGING_DIRECTION {
BACKWARDS = -1,
}
export enum COLOR_FILTER {
NONE = '',
SEPIA = 'filter-sepia',
DARK = 'filter-dark'
}

View File

@ -1,4 +1,4 @@
<div class="reader">
<div class="reader" #reader>
<div class="fixed-top overlay" *ngIf="menuOpen" [@slideFromTop]="menuOpen">
<div style="display: flex; margin-top: 5px;">
<button class="btn btn-icon" style="height: 100%" title="Back" (click)="closeReader()">
@ -24,7 +24,7 @@
</ng-container>
<div (click)="toggleMenu()" class="reading-area">
<canvas #content class="{{getFittingOptionClass()}} {{this.colorMode}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} "
<canvas #content class="{{getFittingOptionClass()}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}"
ondragstart="return false;" onselectstart="return false;">
</canvas>
<div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading">
@ -39,8 +39,18 @@
[bookmarkPage]="showBookmarkEffectEvent"></app-infinite-scroller>
</div>
<ng-container *ngIf="readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD">
<div class="{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')"></div>
<div class="{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')"></div>
<div class="pagination-area {{readerMode === READER_MODE.MANGA_LR ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')">
<div *ngIf="showClickOverlay">
<i class="fa fa-angle-{{readingDirection === ReadingDirection.LeftToRight ? 'double-' : ''}}{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'down'}}"
title="Next Page" aria-hidden="true"></i>
</div>
</div>
<div class="pagination-area {{readerMode === READER_MODE.MANGA_LR ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')">
<div *ngIf="showClickOverlay">
<i class="fa fa-angle-{{readingDirection === ReadingDirection.RightToLeft ? 'double-' : ''}}{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'up'}}"
title="Previous Page" aria-hidden="true"></i>
</div>
</div>
</ng-container>
</div>
@ -78,9 +88,9 @@
</button>
</div>
<div class="col">
<button class="btn btn-icon {{this.colorMode}}" [disabled]="readerMode === READER_MODE.WEBTOON" title="Color Options: {{colorOptionName}}" (click)="toggleColorMode();resetMenuCloseTimer();">
<i class="fa fa-palette" aria-hidden="true"></i>
<span class="sr-only"></span>
<button class="btn btn-icon" title="{{this.isFullscreen ? 'Collapse' : 'Fullscreen'}}" (click)="toggleFullscreen();resetMenuCloseTimer();">
<i class="fa {{this.isFullscreen ? 'fa-compress-alt' : 'fa-expand-alt'}}" aria-hidden="true"></i>
<span class="sr-only">{{this.isFullscreen ? 'Collapse' : 'Fullscreen'}}</span>
</button>
</div>
<div class="col">

View File

@ -6,8 +6,6 @@ $side-width: 25%;
$dash-width: 3px;
$pointer-offset: 5px;
$secondary-color: #CCC;
@media(min-width: 600px) {
.overlay .left .i {
@ -18,7 +16,6 @@ $secondary-color: #CCC;
}
}
.btn-icon {
color: white;
}
@ -35,6 +32,10 @@ canvas {
}
}
.loading {
position: absolute;
left: 48%;
@ -48,14 +49,6 @@ canvas {
white-space: nowrap;
}
.filter-dark {
filter: brightness(0.65);
}
.filter-sepia {
filter: sepia(80%) hue-rotate(349deg) saturate(200%) brightness(0.65);
}
.bottom-menu {
padding: 20px 20px;
}
@ -63,6 +56,7 @@ canvas {
.overlay {
background-color: rgba(0,0,0,0.5);
backdrop-filter: blur(10px);
color: white;
}
@ -226,14 +220,27 @@ canvas {
width: 100%;
}
.highlight {
.pagination-area {
display: flex;
align-items: center;
justify-content: center;
i {
color: white;
font-size: 42px;
}
}
.highlight {
background-color: rgba(65, 225, 100, 0.5) !important;
animation: fadein .5s both;
}
.highlight-2 {
backdrop-filter: blur(10px);
}
.highlight-2 {
background-color: rgba(65, 105, 225, 0.5) !important;
animation: fadein .5s both;
}
backdrop-filter: blur(10px);
}
.bookmark-effect {

View File

@ -1,5 +1,5 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { Location } from '@angular/common';
import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { DOCUMENT, Location } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { take, takeUntil } from 'rxjs/operators';
import { User } from '../_models/user';
@ -19,7 +19,7 @@ import { Stack } from '../shared/data-structures/stack';
import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { ChapterInfo } from './_models/chapter-info';
import { COLOR_FILTER, FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
import { pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
import { READER_MODE } from '../_models/preferences/reader-mode';
import { MangaFormat } from '../_models/manga-format';
@ -32,7 +32,7 @@ const CHAPTER_ID_NOT_FETCHED = -2;
const CHAPTER_ID_DOESNT_EXIST = -1;
const ANIMATION_SPEED = 200;
const OVERLAY_AUTO_CLOSE_TIME = 6000;
const OVERLAY_AUTO_CLOSE_TIME = 3000;
const CLICK_OVERLAY_TIMEOUT = 3000;
@ -99,7 +99,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
pageSplitOption = PageSplitOption.FitSplit;
currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT;
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
colorMode: COLOR_FILTER = COLOR_FILTER.NONE;
isFullscreen: boolean = false;
autoCloseMenu: boolean = true;
readerMode: READER_MODE = READER_MODE.MANGA_LR;
@ -107,6 +107,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
isLoading = true;
@ViewChild('reader') reader!: ElementRef;
@ViewChild('content') canvas: ElementRef | undefined;
private ctx!: CanvasRenderingContext2D;
private canvasImage = new Image();
@ -249,17 +250,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
get colorOptionName() {
switch(this.colorMode) {
case COLOR_FILTER.NONE:
return 'None';
case COLOR_FILTER.DARK:
return 'Dark';
case COLOR_FILTER.SEPIA:
return 'Sepia';
}
}
get READER_MODE(): typeof READER_MODE {
return READER_MODE;
}
@ -277,7 +267,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private formBuilder: FormBuilder, private navService: NavService,
private toastr: ToastrService, private memberService: MemberService,
private libraryService: LibraryService, private utilityService: UtilityService,
private renderer: Renderer2) {
private renderer: Renderer2, @Inject(DOCUMENT) private document: Document) {
this.navService.hideNavBar();
}
@ -325,6 +315,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.updateForm();
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => {
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
const needsSplitting = this.isCoverImage();
@ -365,6 +356,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.onDestroy.complete();
this.goToPageEvent.complete();
this.showBookmarkEffectEvent.complete();
this.readerService.exitFullscreen();
}
@HostListener('window:keyup', ['$event'])
@ -407,6 +399,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
clickOverlayClass(side: 'right' | 'left') {
if (!this.showClickOverlay) {
return '';
}
if (this.readingDirection === ReadingDirection.LeftToRight) {
return side === 'right' ? 'highlight' : 'highlight-2';
}
return side === 'right' ? 'highlight-2' : 'highlight';
}
init() {
this.nextChapterId = CHAPTER_ID_NOT_FETCHED;
this.prevChapterId = CHAPTER_ID_NOT_FETCHED;
@ -631,6 +634,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
/**
*
* @returns If the current model reflects no split of fit split
* @remarks Fit to Screen falls under no split
*/
isNoSplit() {
const splitValue = parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10);
@ -832,9 +836,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (needsScaling) {
this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384;
this.canvas.nativeElement.height = isSafari ? 4_096 : 16_384;
} else if (this.isCoverImage()) {
//this.canvas.nativeElement.width = this.canvasImage.width / 2;
//this.canvas.nativeElement.height = this.canvasImage.height;
} else {
this.canvas.nativeElement.width = this.canvasImage.width;
this.canvas.nativeElement.height = this.canvasImage.height;
@ -883,11 +884,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// Optimization: When the screen is larger than newWidth, allow no split rendering to occur for a better fit
if (windowWidth > newWidth) {
//console.log('Using raw draw');
this.setCanvasSize();
this.ctx.drawImage(this.canvasImage, 0, 0);
} else {
//console.log('Using scaled draw');
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.ctx.drawImage(this.canvasImage, 0, 0, newWidth, newHeight);
}
} else {
//console.log('Normal Render')
this.ctx.drawImage(this.canvasImage, 0, 0);
}
}
@ -984,16 +990,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
clickOverlayClass(side: 'right' | 'left') {
if (!this.showClickOverlay) {
return '';
}
if (this.readingDirection === ReadingDirection.LeftToRight) {
return side === 'right' ? 'highlight' : 'highlight-2';
}
return side === 'right' ? 'highlight-2' : 'highlight';
}
sliderDragUpdate(context: ChangeContext) {
// This will update the value for value except when in webtoon due to how the webtoon reader
@ -1079,20 +1075,24 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return goToPageNum;
}
toggleColorMode() {
switch(this.colorMode) {
case COLOR_FILTER.NONE:
this.colorMode = COLOR_FILTER.DARK;
break;
case COLOR_FILTER.DARK:
this.colorMode = COLOR_FILTER.SEPIA;
break;
case COLOR_FILTER.SEPIA:
this.colorMode = COLOR_FILTER.NONE;
break;
toggleFullscreen() {
this.isFullscreen = this.readerService.checkFullscreenMode();
if (this.isFullscreen) {
this.readerService.exitFullscreen(() => {
this.isFullscreen = false;
this.firstPageRendered = false;
this.render();
});
} else {
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
this.isFullscreen = true;
this.firstPageRendered = false;
this.render();
});
}
}
toggleReaderMode() {
switch(this.readerMode) {
case READER_MODE.MANGA_LR:
@ -1163,4 +1163,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.toastr.info('Incognito mode is off. Progress will now start being tracked.');
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
}
getWindowDimensions() {
const windowWidth = window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
const windowHeight = window.innerHeight
|| document.documentElement.clientHeight
|| document.body.clientHeight;
return [windowWidth, windowHeight];
}
}

View File

@ -2,7 +2,7 @@
<h2>User Dashboard</h2>
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs nav-pills">
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | titlecase }}</a>
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | sentenceCase }}</a>
<ng-template ngbNavContent>
<ng-container *ngIf="tab.fragment === ''">
<p>
@ -48,7 +48,7 @@
</ng-template>
<ng-template ngbPanelContent>
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
<h3 id="manga-header">Manga</h3>
<h3 id="manga-header">Image Reader</h3>
<div class="form-group">
<label for="settings-reading-direction">Reading Direction</label>&nbsp;<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>
@ -95,7 +95,7 @@
</div>
</div>
<hr>
<h3>Books</h3>
<h3>Book Reader</h3>
<div class="form-group">
<label id="dark-mode-label">Dark Mode</label>
<div class="form-group">
@ -166,15 +166,28 @@
</ng-template>
</ngb-panel>
<!--
<ngb-panel id="password-panel" title="Password">
<ngb-panel id="api-panel" title="3rd Party Clients">
<ng-template ngbPanelHeader>
<div class="d-flex align-items-center justify-content-between">
<button ngbPanelToggle class="btn container-fluid text-left pl-0 accordion-header">Password</button>
<span class="pull-right"><i class="fa fa-angle-{{acc.isExpanded('password-panel') ? 'down' : 'up'}}" aria-hidden="true"></i></span>
<button ngbPanelToggle class="btn container-fluid text-left pl-0 accordion-header">3rd Party Clients</button>
<span class="pull-right"><i class="fa fa-angle-{{acc.isExpanded('api-panel') ? 'down' : 'up'}}" aria-hidden="true"></i></span>
</div>
</ng-template>
<ng-template ngbPanelContent>
<p>All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.</p>
<p class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">OPDS is not enabled on this server.</p>
<app-api-key tooltipText="The API key is like a password. Keep it secret, Keep it safe."></app-api-key>
<app-api-key title="OPDS URL" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
</ng-template>
</ngb-panel> -->
</ngb-accordion>
</ng-container>
<ng-container *ngIf="tab.fragment === 'bookmarks'">
<app-series-bookmarks></app-series-bookmarks>
</ng-container>
<ng-container *ngIf="tab.fragment === 'password'">
<ng-container *ngIf="isAuthenticationEnabled || isAdmin; else authDisabled">
<p>Change your Password</p>
<div class="alert alert-danger" role="alert" *ngIf="resetPasswordErrors.length > 0">
@ -211,26 +224,12 @@
<ng-template #authDisabled>
<p class="text-warning">Authentication is disabled on this server. A password is not required to authenticate.</p>
</ng-template>
</ng-template>
</ngb-panel>
<ngb-panel id="api-panel" title="3rd Party Clients">
<ng-template ngbPanelHeader>
<div class="d-flex align-items-center justify-content-between">
<button ngbPanelToggle class="btn container-fluid text-left pl-0 accordion-header">3rd Party Clients</button>
<span class="pull-right"><i class="fa fa-angle-{{acc.isExpanded('api-panel') ? 'down' : 'up'}}" aria-hidden="true"></i></span>
</div>
</ng-template>
<ng-template ngbPanelContent>
</ng-container>
<ng-container *ngIf="tab.fragment === 'clients'">
<p>All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.</p>
<p class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">OPDS is not enabled on this server.</p>
<app-api-key tooltipText="The API key is like a password. Keep it secret, Keep it safe."></app-api-key>
<app-api-key title="OPDS URL" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
</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>

View File

@ -55,6 +55,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
tabs: Array<{title: string, fragment: string}> = [
{title: 'Preferences', fragment: ''},
{title: 'Bookmarks', fragment: 'bookmarks'},
{title: 'Password', fragment: 'password'},
{title: '3rd Party Clients', fragment: 'clients'},
];
active = this.tabs[0];
opdsEnabled: boolean = false;

View File

@ -7,6 +7,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { NgxSliderModule } from '@angular-slider/ngx-slider';
import { UserSettingsRoutingModule } from './user-settings-routing.module';
import { ApiKeyComponent } from './api-key/api-key.component';
import { SharedModule } from '../shared/shared.module';
@ -24,6 +25,7 @@ import { ApiKeyComponent } from './api-key/api-key.component';
NgbTooltipModule,
NgxSliderModule,
UserSettingsRoutingModule,
SharedModule // SentenceCase pipe
]
})
export class UserSettingsModule { }