Misc Bugfixes and Cleanup (#1144)
* Moved libraryType into chapter info * Fixed a bug where you could not reset cover on a series * Patched in relevant changes from another polish branch * Refactored invite user setup to shift the checking for accessibility to the backend and always show the link. This will help with users who have some unique setups in docker. * Refactored invite user to always print the url to setup a new account. * Single page renderer uses canvasImage rather than re-requesting and relying on cache * Fixed a rendering issue where fit to split on single on a cover wouldn't force width scaling just for that image * Fixed a rendering bug with split image functionality * Added title to copy button * Fixed a bug in GetContinuePoint when a chapter is added to an already read volume and a new chapter is added loose leaf. The loose leaf would be prioritized over the volume chapter. Refactored 2 methods from controller into service and unit tested. * Fixed a bug on opening a volume in series detail that had a chapter added to it after the volume (0 chapter) was read would cause a loose leaf chapter to be opened. * Added mark as read/actionables on Files in volume detail modal. Fixed a bug where we were showing the wrong page count in a volume detail modal. * Removed OnDeck page and replaced it with a pre-filtered All-Series. Hooked up the ability to pass read state to the filter via query params. Fixed some spacing on filter post bootstrap update. * Fixed up some poor documentation on FilterDto.
This commit is contained in:
parent
4b0ed18901
commit
54c1641728
36 changed files with 395 additions and 306 deletions
10
UI/Web/src/app/_models/invite-user-response.ts
Normal file
10
UI/Web/src/app/_models/invite-user-response.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export interface InviteUserResponse {
|
||||
/**
|
||||
* Link to register new user
|
||||
*/
|
||||
emailLink: string;
|
||||
/**
|
||||
* If an email was sent to the invited user
|
||||
*/
|
||||
emailSent: boolean;
|
||||
}
|
|
@ -8,6 +8,7 @@ import { User } from '../_models/user';
|
|||
import { Router } from '@angular/router';
|
||||
import { MessageHubService } from './message-hub.service';
|
||||
import { ThemeService } from '../theme.service';
|
||||
import { InviteUserResponse } from '../_models/invite-user-response';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -134,8 +135,8 @@ export class AccountService implements OnDestroy {
|
|||
return this.httpClient.post<string>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>, sendEmail: boolean}) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/invite', model, {responseType: 'text' as 'json'});
|
||||
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>}) {
|
||||
return this.httpClient.post<InviteUserResponse>(this.baseUrl + 'account/invite', model);
|
||||
}
|
||||
|
||||
confirmEmail(model: {email: string, username: string, password: string, token: string}) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import { InviteUserComponent } from './invite-user/invite-user.component';
|
|||
import { RoleSelectorComponent } from './role-selector/role-selector.component';
|
||||
import { LibrarySelectorComponent } from './library-selector/library-selector.component';
|
||||
import { EditUserComponent } from './edit-user/edit-user.component';
|
||||
import { UserSettingsModule } from '../user-settings/user-settings.module';
|
||||
|
||||
|
||||
|
||||
|
@ -49,7 +50,8 @@ import { EditUserComponent } from './edit-user/edit-user.component';
|
|||
NgbTooltipModule,
|
||||
NgbDropdownModule,
|
||||
SharedModule,
|
||||
PipeModule
|
||||
PipeModule,
|
||||
UserSettingsModule // API-key componet
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
|
|
|
@ -9,13 +9,7 @@
|
|||
Invite a user to your server. Enter their email in and we will send them an email to create an account.
|
||||
</p>
|
||||
|
||||
<p *ngIf="!checkedAccessibility">
|
||||
<span class="spinner-border text-primary" style="width: 1.5rem; height: 1.5rem;" role="status" aria-hidden="true"></span>
|
||||
Checking accessibility of server...
|
||||
</p>
|
||||
|
||||
|
||||
<form [formGroup]="inviteForm">
|
||||
<form [formGroup]="inviteForm" *ngIf="emailLink === ''">
|
||||
<div class="row g-0">
|
||||
<div class="mb-3" style="width:100%">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
|
@ -28,11 +22,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="emailLink !== '' && checkedAccessibility && !accessible">
|
||||
<p>Use this link to finish setting up the user account due to your server not being accessible outside your local network.</p>
|
||||
<a class="email-link" href="{{emailLink}}" target="_blank">{{emailLink}}</a>
|
||||
</ng-container>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6">
|
||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true"></app-role-selector>
|
||||
|
@ -44,12 +33,21 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<ng-container *ngIf="emailLink !== ''">
|
||||
<h4>User invited</h4>
|
||||
<p>You can use the following link below to setup the account for your user or use the copy button. You may need to log out before using the link to register a new user.
|
||||
If your server is externallyaccessible, an email will have been sent to the user and the links can be used by them to finish setting up their account.
|
||||
</p>
|
||||
<a class="email-link" href="{{emailLink}}" target="_blank">Setup user's account</a>
|
||||
<app-api-key title="Invite Url" tooltipText="Copy this and paste in a new tab. You may need to log out." [showRefresh]="false" [transform]="makeLink"></app-api-key>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" (click)="invite()" [disabled]="isSending || !inviteForm.valid || !checkedAccessibility || emailLink !== ''">
|
||||
<button type="button" class="btn btn-primary" (click)="invite()" [disabled]="isSending || !inviteForm.valid || emailLink !== ''">
|
||||
<span *ngIf="isSending" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span>{{isSending ? 'Inviting...' : 'Invite'}}</span>
|
||||
</button>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { InviteUserResponse } from 'src/app/_models/invite-user-response';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
|
@ -19,15 +20,12 @@ export class InviteUserComponent implements OnInit {
|
|||
*/
|
||||
isSending: boolean = false;
|
||||
inviteForm: FormGroup = new FormGroup({});
|
||||
/**
|
||||
* If a user would be able to load this server up externally
|
||||
*/
|
||||
accessible: boolean = true;
|
||||
checkedAccessibility: boolean = false;
|
||||
selectedRoles: Array<string> = [];
|
||||
selectedLibraries: Array<number> = [];
|
||||
emailLink: string = '';
|
||||
|
||||
makeLink: (val: string) => string = (val: string) => {return this.emailLink};
|
||||
|
||||
public get email() { return this.inviteForm.get('email'); }
|
||||
|
||||
constructor(public modal: NgbActiveModal, private accountService: AccountService, private serverService: ServerService,
|
||||
|
@ -35,14 +33,6 @@ export class InviteUserComponent implements OnInit {
|
|||
|
||||
ngOnInit(): void {
|
||||
this.inviteForm.addControl('email', new FormControl('', [Validators.required]));
|
||||
|
||||
this.serverService.isServerAccessible().subscribe(async (accessibile) => {
|
||||
if (!accessibile) {
|
||||
await this.confirmService.alert('This server is not accessible outside the network. You cannot invite via Email. You wil be given a link to finish registration with instead.');
|
||||
this.accessible = accessibile;
|
||||
}
|
||||
this.checkedAccessibility = true;
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
|
@ -57,11 +47,10 @@ export class InviteUserComponent implements OnInit {
|
|||
email,
|
||||
libraries: this.selectedLibraries,
|
||||
roles: this.selectedRoles,
|
||||
sendEmail: this.accessible
|
||||
}).subscribe(emailLink => {
|
||||
this.emailLink = emailLink;
|
||||
}).subscribe((data: InviteUserResponse) => {
|
||||
this.emailLink = data.emailLink;
|
||||
this.isSending = false;
|
||||
if (this.accessible) {
|
||||
if (data.emailSent) {
|
||||
this.toastr.info('Email sent to ' + email);
|
||||
this.modal.close(true);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { take, debounceTime, takeUntil } from 'rxjs/operators';
|
|||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||
import { FilterSettings } from '../cards/card-detail-layout/card-detail-layout.component';
|
||||
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
|
||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||
import { Library } from '../_models/library';
|
||||
import { Pagination } from '../_models/pagination';
|
||||
import { Series } from '../_models/series';
|
||||
|
|
|
@ -6,7 +6,6 @@ import { RecentlyAddedComponent } from './recently-added/recently-added.componen
|
|||
import { UserLoginComponent } from './user-login/user-login.component';
|
||||
import { AuthGuard } from './_guards/auth.guard';
|
||||
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
||||
import { OnDeckComponent } from './on-deck/on-deck.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { AllSeriesComponent } from './all-series/all-series.component';
|
||||
import { AdminGuard } from './_guards/admin.guard';
|
||||
|
@ -64,7 +63,6 @@ const routes: Routes = [
|
|||
children: [
|
||||
{path: 'library', component: DashboardComponent},
|
||||
{path: 'recently-added', component: RecentlyAddedComponent},
|
||||
{path: 'on-deck', component: OnDeckComponent},
|
||||
{path: 'all-series', component: AllSeriesComponent},
|
||||
|
||||
]
|
||||
|
|
|
@ -22,7 +22,6 @@ import { CarouselModule } from './carousel/carousel.module';
|
|||
|
||||
import { TypeaheadModule } from './typeahead/typeahead.module';
|
||||
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
||||
import { OnDeckComponent } from './on-deck/on-deck.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { CardsModule } from './cards/cards.module';
|
||||
import { CollectionsModule } from './collections/collections.module';
|
||||
|
@ -48,7 +47,6 @@ import { ColorPickerModule } from 'ngx-color-picker';
|
|||
SeriesDetailComponent,
|
||||
ReviewSeriesModalComponent,
|
||||
RecentlyAddedComponent,
|
||||
OnDeckComponent,
|
||||
DashboardComponent,
|
||||
NavEventsToggleComponent,
|
||||
SeriesMetadataDetailComponent,
|
||||
|
@ -99,7 +97,6 @@ import { ColorPickerModule } from 'ngx-color-picker';
|
|||
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
||||
Title,
|
||||
{provide: SAVER, useFactory: getSaver},
|
||||
// { provide: APP_BASE_HREF, useFactory: (config: ConfigData) => config.baseUrl, deps: [ConfigData] },
|
||||
],
|
||||
entryComponents: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<span>
|
||||
<span *ngIf="chapterMetadata && chapterMetadata.releaseDate !== null">Release Date: {{chapterMetadata.releaseDate | date: 'shortDate' || '-'}}</span>
|
||||
</span>
|
||||
<span class="text-accent">{{chapter.pages}} pages</span>
|
||||
<span class="text-accent">{{data.pages}} pages</span>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div class="col-auto">
|
||||
|
@ -106,24 +106,26 @@
|
|||
<h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{utilityService.formatChapterName(libraryType, true, false)}} {{formatChapterNumber(chapter)}}">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">
|
||||
<span *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
<span >
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</ng-container>
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill">
|
||||
<span class="badge bg-primary rounded-pill ms-1">
|
||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
|
||||
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #specialHeader>File(s)</ng-template>
|
||||
<ng-template #specialHeader>Files</ng-template>
|
||||
</h5>
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let file of chapter.files" class="list-group-item no-hover">
|
||||
|
|
|
@ -67,6 +67,8 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
publicationStatuses: Array<PublicationStatusDto> = [];
|
||||
validLanguages: Array<Language> = [];
|
||||
|
||||
coverImageReset = false;
|
||||
|
||||
get Breakpoint(): typeof Breakpoint {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
@ -403,30 +405,22 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.seriesService.updateMetadata(this.metadata, this.collectionTags)
|
||||
];
|
||||
|
||||
// We only need to call updateSeries if we changed name, sort name, or localized name
|
||||
if (this.editSeriesForm.get('name')?.dirty || this.editSeriesForm.get('sortName')?.dirty || this.editSeriesForm.get('localizedName')?.dirty) {
|
||||
// We only need to call updateSeries if we changed name, sort name, or localized name or reset a cover image
|
||||
if (this.editSeriesForm.get('name')?.dirty || this.editSeriesForm.get('sortName')?.dirty || this.editSeriesForm.get('localizedName')?.dirty || this.coverImageReset) {
|
||||
apis.push(this.seriesService.updateSeries(model));
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (selectedIndex > 0) {
|
||||
if (selectedIndex > 0 && this.selectedCover !== '') {
|
||||
apis.push(this.uploadService.updateSeriesCoverImage(model.id, this.selectedCover));
|
||||
}
|
||||
|
||||
|
||||
forkJoin(apis).subscribe(results => {
|
||||
this.modal.close({success: true, series: model, coverImageUpdate: selectedIndex > 0});
|
||||
});
|
||||
}
|
||||
|
||||
handleUnlock(field: string) {
|
||||
console.log('todo: unlock ', field);
|
||||
}
|
||||
|
||||
hello(val: boolean) {
|
||||
console.log('hello: ', val);
|
||||
}
|
||||
|
||||
updateCollections(tags: CollectionTag[]) {
|
||||
this.collectionTags = tags;
|
||||
}
|
||||
|
@ -491,6 +485,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
handleReset() {
|
||||
this.coverImageReset = true;
|
||||
this.editSeriesForm.patchValue({
|
||||
coverImageLocked: false
|
||||
});
|
||||
|
|
|
@ -27,9 +27,7 @@
|
|||
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [style.--drawer-background-color]="'#010409'" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
|
||||
<div header>
|
||||
<h2 style="margin-top: 0.5rem">Book Settings
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="commentDrawer.close()">
|
||||
|
||||
</button>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="commentDrawer.close()"></button>
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
|
@ -324,7 +322,7 @@
|
|||
</div>
|
||||
<div class="col-md-2 me-3"></div>
|
||||
</div>
|
||||
<div class="row justify-content-center g-0">
|
||||
<div class="row justify-content-center g-0 mt-2">
|
||||
<div class="col-md-2 me-3" *ngIf="!filterSettings.sortDisabled">
|
||||
<form [formGroup]="sortGroup">
|
||||
<div class="mb-3">
|
||||
|
|
|
@ -113,6 +113,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
|
||||
private utilityService: UtilityService, private collectionTagService: CollectionTagService) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
|
||||
this.readProgressGroup = new FormGroup({
|
||||
read: new FormControl(this.filter.readStatus.read, []),
|
||||
notRead: new FormControl(this.filter.readStatus.notRead, []),
|
||||
|
@ -163,6 +164,12 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||
this.filterSettings = new FilterSettings();
|
||||
}
|
||||
|
||||
if (this.filterSettings.presets) {
|
||||
this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets?.readStatus.read);
|
||||
this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets?.readStatus.notRead);
|
||||
this.readProgressGroup.get('inProgress')?.patchValue(this.filterSettings.presets?.readStatus.inProgress);
|
||||
}
|
||||
|
||||
this.setupTypeaheads();
|
||||
}
|
||||
|
||||
|
|
|
@ -141,7 +141,11 @@ export class LibraryComponent implements OnInit, OnDestroy {
|
|||
} else if (sectionTitle.toLowerCase() === 'recently updated series') {
|
||||
this.router.navigate(['recently-added']);
|
||||
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
||||
this.router.navigate(['on-deck']);
|
||||
const params: any = {};
|
||||
params['readStatus'] = 'true,false,false';
|
||||
params['page'] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
//this.router.navigate(['on-deck']);
|
||||
} else if (sectionTitle.toLowerCase() === 'libraries') {
|
||||
this.router.navigate(['all-series']);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { LibraryType } from "src/app/_models/library";
|
||||
import { MangaFormat } from "src/app/_models/manga-format";
|
||||
|
||||
export interface ChapterInfo {
|
||||
|
@ -8,6 +9,7 @@ export interface ChapterInfo {
|
|||
seriesFormat: MangaFormat;
|
||||
seriesId: number;
|
||||
libraryId: number;
|
||||
libraryType: LibraryType;
|
||||
fileName: string;
|
||||
isSpecial: boolean;
|
||||
volumeId: number;
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
</canvas>
|
||||
</div>
|
||||
<div class="image-container" [ngClass]="{'d-none': renderWithCanvas, 'center-double': ShouldRenderDoublePage, 'fit-to-width-double-offset' : this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.WIDTH && ShouldRenderDoublePage, 'fit-to-height-double-offset': this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.HEIGHT && ShouldRenderDoublePage, 'original-double-offset' : this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage, 'reverse': ShouldRenderReverseDouble}">
|
||||
<img [src]="readerService.getPageUrl(this.chapterId, this.pageNum)" id="image-1"
|
||||
<img [src]="canvasImage.src" id="image-1"
|
||||
class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
||||
|
||||
<ng-container *ngIf="ShouldRenderDoublePage && (this.pageNum + 1 <= maxPages - 1 && this.pageNum > 0)">
|
||||
|
@ -128,27 +128,19 @@
|
|||
</div>
|
||||
<div class="bottom-menu" *ngIf="settingsOpen && generalSettingsForm">
|
||||
<form [formGroup]="generalSettingsForm">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<label for="page-splitting" class="form-label">Image Splitting</label>
|
||||
<div class="split fa fa-image">
|
||||
<div class="{{splitIconClass}}"></div>
|
||||
</div>
|
||||
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
|
||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
|
||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<label for="page-fitting" class="form-label">Image Scaling</label> <i class="fa {{getFittingIcon()}}" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<select class="form-control" id="page-fitting" formControlName="fittingOption">
|
||||
<option value="full-height">Height</option>
|
||||
<option value="full-width">Width</option>
|
||||
|
@ -157,11 +149,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="autoCloseMenu" class="form-check-label">Auto Close Menu</label>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<label for="layout-mode" class="form-label">Layout Mode</label>
|
||||
<select class="form-control" id="page-fitting" formControlName="layoutMode">
|
||||
<option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="mb-3">
|
||||
<label id="auto-close-label" class="form-label"></label>
|
||||
<div class="mb-3">
|
||||
|
@ -173,17 +168,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="layout-mode" class="form-label">Layout Mode</label>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<select class="form-control" id="page-fitting" formControlName="layoutMode">
|
||||
<option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -47,9 +47,9 @@ img {
|
|||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
}
|
||||
// canvas {
|
||||
// //position: absolute; // JOE: Not sure why we have this, but it breaks the renderer
|
||||
// }
|
||||
|
||||
.reader {
|
||||
background-color: var(--manga-reader-bg-color);
|
||||
|
|
|
@ -122,7 +122,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
* Used soley for LayoutMode.Double rendering. Will always hold the next image in buffer.
|
||||
*/
|
||||
canvasImage2 = new Image();
|
||||
renderWithCanvas: boolean = false; // Dictates if we use render with canvas or with image
|
||||
/**
|
||||
* Dictates if we use render with canvas or with image. This is only for Splitting.
|
||||
*/
|
||||
renderWithCanvas: boolean = false;
|
||||
|
||||
/**
|
||||
* A circular array of size PREFETCH_PAGES + 2. Maintains prefetched Images around the current page to load from to avoid loading animation.
|
||||
|
@ -530,11 +533,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
newOptions.ceil = this.maxPages - 1; // We -1 so that the slider UI shows us hitting the end, since visually we +1 everything.
|
||||
this.pageOptions = newOptions;
|
||||
|
||||
// TODO: Move this into ChapterInfo
|
||||
this.libraryService.getLibraryType(results.chapterInfo.libraryId).pipe(take(1)).subscribe(type => {
|
||||
this.libraryType = type;
|
||||
this.updateTitle(results.chapterInfo, type);
|
||||
});
|
||||
this.libraryType = results.chapterInfo.libraryType;
|
||||
this.updateTitle(results.chapterInfo, this.libraryType);
|
||||
|
||||
this.inSetup = false;
|
||||
|
||||
|
@ -645,20 +645,20 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
getFittingOptionClass() {
|
||||
const formControl = this.generalSettingsForm.get('fittingOption');
|
||||
let val = FITTING_OPTION.HEIGHT;
|
||||
let val = FITTING_OPTION.HEIGHT as string;
|
||||
if (formControl === undefined) {
|
||||
val = FITTING_OPTION.HEIGHT;
|
||||
val = FITTING_OPTION.HEIGHT as string;
|
||||
}
|
||||
val = formControl?.value;
|
||||
|
||||
|
||||
if (this.isCoverImage() && this.layoutMode !== LayoutMode.Single) {
|
||||
return val + ' cover double';
|
||||
if (this.layoutMode !== LayoutMode.Single) {
|
||||
val = val + (this.isCoverImage() ? 'cover' : '') + 'double';
|
||||
} else if (this.isCoverImage() && this.shouldRenderAsFitSplit()) {
|
||||
// JOE: If we are Fit to Screen, we should use fitting as width just for cover images
|
||||
// Rewriting to fit to width for this cover image
|
||||
val = FITTING_OPTION.WIDTH;
|
||||
}
|
||||
|
||||
if (!this.isCoverImage() && this.layoutMode !== LayoutMode.Single) {
|
||||
return val + ' double';
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
|
@ -971,6 +971,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.renderWithCanvas = true;
|
||||
} else {
|
||||
this.renderWithCanvas = false;
|
||||
|
||||
// if (this.isCoverImage() && this.layoutMode === LayoutMode.Single && this.getFit() !== FITTING_OPTION.WIDTH) {
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
// Reset scroll on non HEIGHT Fits
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<app-card-detail-layout header="On Deck"
|
||||
[isLoading]="isLoading"
|
||||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()" (selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
|
@ -1,132 +0,0 @@
|
|||
import { Component, HostListener, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||
import { FilterSettings } from '../cards/card-detail-layout/card-detail-layout.component';
|
||||
import { KEY_CODES } from '../shared/_services/utility.service';
|
||||
import { Pagination } from '../_models/pagination';
|
||||
import { Series } from '../_models/series';
|
||||
import { FilterEvent, SeriesFilter} from '../_models/series-filter';
|
||||
import { Action } from '../_services/action-factory.service';
|
||||
import { ActionService } from '../_services/action.service';
|
||||
import { SeriesService } from '../_services/series.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-on-deck',
|
||||
templateUrl: './on-deck.component.html',
|
||||
styleUrls: ['./on-deck.component.scss']
|
||||
})
|
||||
export class OnDeckComponent implements OnInit {
|
||||
|
||||
isLoading: boolean = true;
|
||||
series: Series[] = [];
|
||||
pagination!: Pagination;
|
||||
libraryId!: number;
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
filterSettings: FilterSettings = new FilterSettings();
|
||||
|
||||
constructor(private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private titleService: Title,
|
||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.titleService.setTitle('Kavita - On Deck');
|
||||
if (this.pagination === undefined || this.pagination === null) {
|
||||
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
}
|
||||
this.filterSettings.readProgressDisabled = true;
|
||||
this.filterSettings.sortDisabled = true;
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.shift', ['$event'])
|
||||
handleKeypress(event: KeyboardEvent) {
|
||||
if (event.key === KEY_CODES.SHIFT) {
|
||||
this.bulkSelectionService.isShiftDown = true;
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:keyup.shift', ['$event'])
|
||||
handleKeyUp(event: KeyboardEvent) {
|
||||
if (event.key === KEY_CODES.SHIFT) {
|
||||
this.bulkSelectionService.isShiftDown = false;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
seriesClicked(series: Series) {
|
||||
this.router.navigate(['library', this.libraryId, 'series', series.id]);
|
||||
}
|
||||
|
||||
onPageChange(pagination: Pagination) {
|
||||
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage);
|
||||
this.loadPage();
|
||||
}
|
||||
|
||||
updateFilter(event: FilterEvent) {
|
||||
this.filter = event.filter;
|
||||
const page = this.getPage();
|
||||
if (page === undefined || page === null || !event.isFirst) {
|
||||
this.pagination.currentPage = 1;
|
||||
this.onPageChange(this.pagination);
|
||||
} else {
|
||||
this.loadPage();
|
||||
}
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
const page = this.getPage();
|
||||
if (page != null) {
|
||||
this.pagination.currentPage = parseInt(page, 10);
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.seriesService.getOnDeck(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
this.pagination = series.pagination;
|
||||
this.isLoading = false;
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
getPage() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('page');
|
||||
}
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series');
|
||||
const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + ''));
|
||||
|
||||
switch (action) {
|
||||
case Action.AddToReadingList:
|
||||
this.actionService.addMultipleSeriesToReadingList(selectedSeries, () => {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
break;
|
||||
case Action.AddToCollection:
|
||||
this.actionService.addMultipleSeriesToCollectionTag(selectedSeries, () => {
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
break;
|
||||
case Action.MarkAsRead:
|
||||
this.actionService.markMultipleSeriesAsRead(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
break;
|
||||
case Action.MarkAsUnread:
|
||||
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
break;
|
||||
case Action.Delete:
|
||||
this.actionService.deleteMultipleSeries(selectedSeries, () => {
|
||||
this.loadPage();
|
||||
this.bulkSelectionService.deselectAll();
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -5,7 +5,6 @@
|
|||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6">
|
||||
<div class="row g-0">
|
||||
|
||||
<h2>
|
||||
{{series?.name}}
|
||||
</h2>
|
||||
|
|
|
@ -496,7 +496,12 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
// If user has progress on the volume, load them where they left off
|
||||
if (volume.pagesRead < volume.pages && volume.pagesRead > 0) {
|
||||
// Find the continue point chapter and load it
|
||||
this.readerService.getCurrentChapter(this.seriesId).subscribe(chapter => this.openChapter(chapter));
|
||||
const unreadChapters = volume.chapters.filter(item => item.pagesRead < item.pages);
|
||||
if (unreadChapters.length > 0) {
|
||||
this.openChapter(unreadChapters[0]);
|
||||
return;
|
||||
}
|
||||
this.openChapter(volume.chapters[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -509,7 +514,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
openViewInfo(data: Volume | Chapter) {
|
||||
const modalRef = this.modalService.open(CardDetailsModalComponent, { size: 'lg' }); // , scrollable: true (these don't work well on mobile)
|
||||
const modalRef = this.modalService.open(CardDetailsModalComponent, { size: 'lg' });
|
||||
modalRef.componentInstance.data = data;
|
||||
modalRef.componentInstance.parentName = this.series?.name;
|
||||
modalRef.componentInstance.seriesId = this.series?.id;
|
||||
|
|
|
@ -207,6 +207,18 @@ export class UtilityService {
|
|||
filter.translators = [...filter.translators, ...translators.split(',').map(item => parseInt(item, 10))];
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
/// Read status is encoded as true,true,true
|
||||
const readStatus = snapshot.queryParamMap.get('readStatus');
|
||||
if (readStatus !== undefined && readStatus !== null) {
|
||||
const values = readStatus.split(',').map(i => i === "true");
|
||||
if (values.length === 3) {
|
||||
filter.readStatus.inProgress = values[0];
|
||||
filter.readStatus.notRead = values[1];
|
||||
filter.readStatus.read = values[2];
|
||||
anyChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return [filter, anyChanged];
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="input-group">
|
||||
<input #apiKey type="text" readonly class="form-control" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||
<div id="button-addon4">
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="copy()"><span class="visually-hidden">Copy</span><i class="fa fa-copy" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="copy()" title="Copy"><span class="visually-hidden">Copy</span><i class="fa fa-copy" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-danger" type="button" [ngbTooltip]="tipContent" (click)="refresh()" *ngIf="showRefresh"><span class="visually-hidden">Regenerate</span><i class="fa fa-sync-alt" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<ng-template #tipContent>
|
||||
|
|
|
@ -31,12 +31,12 @@ import { ColorPickerModule } from 'ngx-color-picker';
|
|||
NgbTooltipModule,
|
||||
NgxSliderModule,
|
||||
UserSettingsRoutingModule,
|
||||
//SharedModule, // SentenceCase pipe
|
||||
PipeModule,
|
||||
ColorPickerModule, // User prefernces background color
|
||||
],
|
||||
exports: [
|
||||
SiteThemeProviderPipe
|
||||
SiteThemeProviderPipe,
|
||||
ApiKeyComponent
|
||||
]
|
||||
})
|
||||
export class UserSettingsModule { }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue