Feature/enhancements and more (#1166)

* 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.

* Some string equals enhancements to reduce extra allocations

* Fixed an issue when trying to download via a url, to remove query parameters to get the format

* Made an optimization to Normalize method to reduce memory pressure by 100MB over the course of a scan (16k files)

* Adjusted the styles on dashboard for first time setup and used a routerlink rather than href to avoid a fresh load.

* Use framgment on router link

* Hooked in the ability to search by release year (along with series optionally) and series will be returned back.

* Fixed a bug in the filter format code where it was sending the wrong type

* Only show clear all on typeahead when there are at least one selected item

* Cleaned up the styles of the styles of the typeahead

* Removed some dead code

* Implemented the ability to filter against a series name.

* Fixed filter top offset

* Ensure that when we add or remove libraries, the side nav of users gets updated.

* Tweaked the width on the mobile side nav

* Close side nav on clicking overlay on mobile viewport

* Don't show a pointer if the carousel section title is not actually selectable

* Removed the User profile on the side nav so home is always first. Tweaked styles to match

* Fixed up some poor documentation on FilterDto.

* Fixed a bug where Latest read date wasn't being set due to an early short circuit.

* When sending the chapter file, format the title of the FeedEntry more like Series Detail.

* Removed dead code
This commit is contained in:
Joseph Milazzo 2022-03-21 09:26:49 -05:00 committed by GitHub
parent 67d8d3d808
commit 4a93b5c715
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 663 additions and 451 deletions

View file

@ -0,0 +1,4 @@
export interface LibraryModifiedEvent {
libraryId: number;
action: 'create' | 'delelte';
}

View 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;
}

View file

@ -29,6 +29,7 @@ export interface SeriesFilter {
tags: Array<number>;
languages: Array<string>;
publicationStatus: Array<number>;
seriesNameQuery: string;
}
export interface SortOptions {

View file

@ -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'
@ -130,8 +131,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}) {

View file

@ -4,6 +4,7 @@ import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import { ToastrService } from 'ngx-toastr';
import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { environment } from 'src/environments/environment';
import { LibraryModifiedEvent } from '../_models/events/library-modified-event';
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
import { SiteThemeProgressEvent } from '../_models/events/site-theme-progress-event';
import { User } from '../_models/user';
@ -49,6 +50,10 @@ export enum EVENTS {
* A subtype of NotificationProgress that represents a file being processed for cover image extraction
*/
CoverUpdateProgress = 'CoverUpdateProgress',
/**
* A library is created or removed from the instance
*/
LibraryModified = 'LibraryModified'
}
export interface Message<T> {
@ -130,6 +135,13 @@ export class MessageHubService {
});
});
this.hubConnection.on(EVENTS.LibraryModified, resp => {
this.messagesSource.next({
event: EVENTS.LibraryModified,
payload: resp.body as LibraryModifiedEvent
});
});
this.hubConnection.on(EVENTS.NotificationProgress, (resp: NotificationProgressEvent) => {
this.messagesSource.next({

View file

@ -226,6 +226,7 @@ export class SeriesService {
tags: [],
languages: [],
publicationStatus: [],
seriesNameQuery: '',
};
if (filter === undefined) return data;

View file

@ -13,12 +13,12 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ResetPasswordModalComponent } from './_modals/reset-password-modal/reset-password-modal.component';
import { ManageSettingsComponent } from './manage-settings/manage-settings.component';
import { ManageSystemComponent } from './manage-system/manage-system.component';
import { ChangelogComponent } from '../announcements/changelog/changelog.component';
import { PipeModule } from '../pipe/pipe.module';
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';
import { SidenavModule } from '../sidenav/sidenav.module';
@ -50,7 +50,8 @@ import { SidenavModule } from '../sidenav/sidenav.module';
NgbDropdownModule,
SharedModule,
PipeModule,
SidenavModule
SidenavModule,
UserSettingsModule // API-key componet
],
providers: []
})

View file

@ -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>
&nbsp;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>

View file

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

View file

@ -3,7 +3,7 @@
<app-card-actionables [actions]="actions"></app-card-actionables>
All Series
</h2>
<h6 subtitle style="margin-left:40px;">{{pagination?.totalItems}} Series</h6>
<h6 subtitle>{{pagination?.totalItems}} Series</h6>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout

View file

@ -6,7 +6,6 @@ import { take, debounceTime, takeUntil } from 'rxjs/operators';
import { BulkSelectionService } from '../cards/bulk-selection.service';
import { FilterSettings } from '../metadata-filter/filter-settings';
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';

View file

@ -1,3 +1,7 @@
<h1>Announcements</h1>
<app-side-nav-companion-bar>
<h2 title>
Announcements
</h2>
</app-side-nav-companion-bar>
<app-changelog></app-changelog>

View file

@ -5,6 +5,7 @@ import { ChangelogComponent } from './changelog/changelog.component';
import { AnnouncementsRoutingModule } from './announcements-routing.module';
import { SharedModule } from '../shared/shared.module';
import { PipeModule } from '../pipe/pipe.module';
import { SidenavModule } from '../sidenav/sidenav.module';
@ -17,7 +18,8 @@ import { PipeModule } from '../pipe/pipe.module';
CommonModule,
AnnouncementsRoutingModule,
SharedModule,
PipeModule
PipeModule,
SidenavModule
]
})
export class AnnouncementsModule { }

View file

@ -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';
@ -70,7 +69,6 @@ const routes: Routes = [
children: [
{path: 'library', component: DashboardComponent},
{path: 'recently-added', component: RecentlyAddedComponent},
{path: 'on-deck', component: OnDeckComponent},
{path: 'all-series', component: AllSeriesComponent},
]

View file

@ -23,7 +23,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';
@ -50,7 +49,6 @@ import { SidenavModule } from './sidenav/sidenav.module';
SeriesDetailComponent,
ReviewSeriesModalComponent,
RecentlyAddedComponent,
OnDeckComponent,
DashboardComponent,
EventsWidgetComponent,
SeriesMetadataDetailComponent,
@ -104,7 +102,6 @@ import { SidenavModule } from './sidenav/sidenav.module';
{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]

View file

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

View file

@ -89,7 +89,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
private seriesService: SeriesService,
public utilityService: UtilityService,
private fb: FormBuilder,
public imageService: ImageService,
public imageService: ImageService,
private libraryService: LibraryService,
private collectionService: CollectionTagService,
private uploadService: UploadService,
@ -98,8 +98,6 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id));
this.initSeries = Object.assign({}, this.series);
this.libraryService.getLibraryNames().pipe(takeUntil(this.onDestroy)).subscribe(names => {
this.libraryName = names[this.series.libraryId];
});
@ -107,7 +105,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.editSeriesForm = this.fb.group({
id: new FormControl(this.series.id, []),
summary: new FormControl('', []),
summary: new FormControl('', []),
name: new FormControl(this.series.name, []),
localizedName: new FormControl(this.series.localizedName, []),
sortName: new FormControl(this.series.sortName, []),
@ -125,7 +123,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.metadataService.getAllAgeRatings().subscribe(ratings => {
this.ageRatings = ratings;
});
this.metadataService.getAllPublicationStatus().subscribe(statuses => {
this.publicationStatuses = statuses;
});
@ -166,7 +164,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.metadata.ageRating = parseInt(val + '', 10);
this.metadata.ageRatingLocked = true;
});
this.editSeriesForm.get('publicationStatus')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(val => {
this.metadata.publicationStatus = parseInt(val + '', 10);
this.metadata.publicationStatusLocked = true;
@ -245,8 +243,8 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
return options.filter(m => this.utilityService.filter(m.title, filter));
}
this.tagsSettings.fetchFn = (filter: string) => this.metadataService.getAllTags()
.pipe(map(items => this.tagsSettings.compareFn(items, filter)));
.pipe(map(items => this.tagsSettings.compareFn(items, filter)));
this.tagsSettings.addTransformFn = ((title: string) => {
return {id: 0, title: title };
});
@ -269,7 +267,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.genreSettings.addIfNonExisting = true;
this.genreSettings.fetchFn = (filter: string) => {
return this.metadataService.getAllGenres()
.pipe(map(items => this.genreSettings.compareFn(items, filter)));
.pipe(map(items => this.genreSettings.compareFn(items, filter)));
};
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
return options.filter(m => this.utilityService.filter(m.title, filter));
@ -336,7 +334,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
return forkJoin([
this.updateFromPreset('writer', this.metadata.writers, PersonRole.Writer),
this.updateFromPreset('character', this.metadata.characters, PersonRole.Character),
this.updateFromPreset('character', this.metadata.characters, PersonRole.Character),
this.updateFromPreset('colorist', this.metadata.colorists, PersonRole.Colorist),
this.updateFromPreset('cover-artist', this.metadata.coverArtists, PersonRole.CoverArtist),
this.updateFromPreset('editor', this.metadata.editors, PersonRole.Editor),
@ -350,7 +348,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
}));
}
fetchPeople(role: PersonRole, filter: string) {
fetchPeople(role: PersonRole, filter: string) {
return this.metadataService.getAllPeople().pipe(map(people => {
return people.filter(p => p.role == role && this.utilityService.filter(p.name, filter));
}));
@ -415,7 +413,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
apis.push(this.seriesService.updateSeries(model));
}
if (selectedIndex > 0 && this.selectedCover !== '') {
apis.push(this.uploadService.updateSeriesCoverImage(model.id, this.selectedCover));
}

View file

@ -14,7 +14,7 @@
</div>
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
@ -22,7 +22,7 @@
<div class="col-auto ps-1 pe-1 mt-2 mb-2" *ngFor="let item of items; trackBy:trackByIdentity; index as i">
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
</div>
<p *ngIf="items.length === 0 && !isLoading">
There is no data
</p>
@ -68,7 +68,7 @@
</div>
</li>
</ng-template>
</ngb-pagination>
</div>
</ng-template>

View file

@ -20,7 +20,7 @@ const ANIMATION_SPEED = 300;
export class CardDetailLayoutComponent implements OnInit, OnDestroy {
@Input() header: string = '';
@Input() isLoading: boolean = false;
@Input() isLoading: boolean = false;
@Input() items: any[] = [];
@Input() pagination!: Pagination;
/**
@ -36,7 +36,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
@Output() itemClicked: EventEmitter<any> = new EventEmitter();
@Output() pageChange: EventEmitter<Pagination> = new EventEmitter();
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
@ContentChild('cardItem') itemTemplate!: TemplateRef<any>;

View file

@ -1,6 +1,6 @@
<div class="carousel-container" *ngIf="items.length > 0">
<div>
<h2 style="display: inline-block;"><a href="javascript:void(0)" (click)="sectionClicked($event)" class="section-title">{{title}}</a></h2>
<h3 style="display: inline-block;"><a href="javascript:void(0)" (click)="sectionClicked($event)" class="section-title" [ngClass]="{'non-selectable': !clickableTitle}">{{title}}</a></h3>
<div class="float-end">
<button class="btn btn-icon" [disabled]="isBeginning" (click)="prevPage()"><i class="fa fa-angle-left" aria-hidden="true"></i><span class="visually-hidden">Previous Items</span></button>
<button class="btn btn-icon" [disabled]="isEnd" (click)="nextPage()"><i class="fa fa-angle-right" aria-hidden="true"></i><span class="visually-hidden">Next Items</span></button>

View file

@ -17,6 +17,10 @@
text-decoration: var(--carousel-hover-header-text-decoration);
}
}
.non-selectable {
cursor: default;
}
}

View file

@ -12,6 +12,7 @@ export class CarouselReelComponent implements OnInit {
@ContentChild('carouselItem') carouselItemTemplate!: TemplateRef<any>;
@Input() items: any[] = [];
@Input() title = '';
@Input() clickableTitle: boolean = true;
@Output() sectionClick = new EventEmitter<string>();
swiper: Swiper | undefined;

View file

@ -1,9 +1,13 @@
<div *ngIf="libraries.length === 0 && !isLoading && isAdmin" class="d-flex justify-content-center">
<p>There are no libraries setup yet. Configure some in <a href="/admin/dashboard#libraries">Server settings</a>.</p>
</div>
<div *ngIf="libraries.length === 0 && !isLoading && !isAdmin" class="d-flex justify-content-center">
<p>You haven't been granted access to any libraries.</p>
</div>
<ng-container *ngIf="libraries.length === 0 && !isLoading">
<div class="mt-3">
<div *ngIf="isAdmin" class="d-flex justify-content-center">
<p>There are no libraries setup yet. Configure some in <a routerLink="/admin/dashboard" fragment="libraries">Server settings</a>.</p>
</div>
<div *ngIf="!isAdmin" class="d-flex justify-content-center">
<p>You haven't been granted access to any libraries.</p>
</div>
</div>
</ng-container>
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
<ng-template #carouselItem let-item let-position="idx">
@ -12,14 +16,14 @@
</app-carousel-reel>
<!-- TODO: Refactor this so we can use series actions here -->
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series" (sectionClick)="handleSectionClick($event)">
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series" (sectionClick)="handleSectionClick($event)">
<ng-template #carouselItem let-item let-position="idx">
<app-card-item [entity]="item" [title]="item.seriesName" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
[supressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
</ng-template>
</app-carousel-reel>
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series">
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series" [clickableTitle]="false">
<ng-template #carouselItem let-item let-position="idx">
<app-series-card [data]="item" [libraryId]="item.libraryId" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
</ng-template>

View file

@ -141,7 +141,10 @@ 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});
} else if (sectionTitle.toLowerCase() === 'libraries') {
this.router.navigate(['all-series']);
}

View file

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

View file

@ -37,12 +37,12 @@
ondragstart="return false;" onselectstart="return false;">
</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,
<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)">
@ -132,27 +132,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>&nbsp;
<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>&nbsp;<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>
@ -161,11 +153,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">
@ -177,17 +172,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>

View file

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

View file

@ -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.
@ -339,7 +342,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
this.libraryId = parseInt(libraryId, 10);
this.seriesId = parseInt(seriesId, 10);
@ -534,11 +537,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;
@ -660,14 +660,19 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return FITTING_OPTION.WIDTH;
}
if (this.isCoverImage() && this.layoutMode !== LayoutMode.Single) {
return val + ' cover double';
}
if (!this.isCoverImage() && this.layoutMode !== LayoutMode.Single) {
return val + ' double';
}
// Code from feature/manga-reader. Validate which fix is better
// 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;
// }
return val;
}

View file

@ -5,14 +5,11 @@
</div>
<div class="not-phone-hidden">
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [style.--drawer-background-color]="'#010409'" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [style.--drawer-background-color]="'#010409'" [options]="{topOffset: 56}" (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>
<div body class="drawer-body">
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
@ -306,6 +303,16 @@
<div class="col-md-2 me-3"></div>
</div>
<div class="row justify-content-center g-0">
<div class="col-md-2 me-3">
<form [formGroup]="seriesNameGroup">
<div class="mb-3">
<label for="series-name" class="form-label">Series Name</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="seriesNameFilterTooltip" role="button" tabindex="0"></i>
<span class="visually-hidden" id="filter-series-name-help"><ng-container [ngTemplateOutlet]="seriesNameFilterTooltip"></ng-container></span>
<ng-template #seriesNameFilterTooltip>Series name will filter against Name, Sort Name, or Localized Name</ng-template>
<input type="text" id="series-name" formControlName="seriesNameQuery" class="form-control" aria-describedby="filter-series-name-help">
</div>
</form>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.sortDisabled">
<form [formGroup]="sortGroup">
<div class="mb-3">
@ -326,11 +333,10 @@
</div>
<div class="col-md-2 me-3" *ngIf="filterSettings.sortDisabled"></div>
<div class="col-md-2 me-3"></div>
<div class="col-md-2 me-3"></div>
<div class="col-md-2 me-3">
<div class="col-md-2 me-3 mt-4">
<button class="btn btn-secondary col-12" (click)="clear()">Clear</button>
</div>
<div class="col-md-2 me-3">
<div class="col-md-2 me-3 mt-4">
<button class="btn btn-primary col-12" (click)="apply()">Apply</button>
</div>
</div>

View file

@ -1,8 +1,8 @@
import { Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
import { forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs';
import { UtilityService } from '../shared/_services/utility.service';
import { distinctUntilChanged, forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs';
import { Breakpoint, UtilityService } from '../shared/_services/utility.service';
import { TypeaheadSettings } from '../typeahead/typeahead-settings';
import { CollectionTag } from '../_models/collection-tag';
import { Genre } from '../_models/genre';
@ -66,6 +66,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
readProgressGroup!: FormGroup;
sortGroup!: FormGroup;
seriesNameGroup!: FormGroup;
isAscendingSort: boolean = true;
updateApplied: number = 0;
@ -83,7 +84,8 @@ export class MetadataFilterComponent 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.filter = this.seriesService.createSeriesFilter();
this.readProgressGroup = new FormGroup({
read: new FormControl(this.filter.readStatus.read, []),
notRead: new FormControl(this.filter.readStatus.notRead, []),
@ -94,6 +96,10 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
sortField: new FormControl(this.filter.sortOptions?.sortField || SortField.SortName, []),
});
this.seriesNameGroup = new FormGroup({
seriesNameQuery: new FormControl(this.filter.seriesNameQuery || '', [])
});
this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => {
this.filter.readStatus.read = this.readProgressGroup.get('read')?.value;
this.filter.readStatus.inProgress = this.readProgressGroup.get('inProgress')?.value;
@ -124,6 +130,13 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
}
this.filter.sortOptions.sortField = parseInt(this.sortGroup.get('sortField')?.value, 10);
});
this.seriesNameGroup.get('seriesNameQuery')?.valueChanges.pipe(
map(val => (val || '').trim()),
distinctUntilChanged(),
takeUntil(this.onDestory)).subscribe(changes => {
this.filter.seriesNameQuery = changes;
});
}
ngOnInit(): void {
@ -137,6 +150,12 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
});
}
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();
}
@ -443,8 +462,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
return personSettings;
}
updateFormatFilters(formats: MangaFormat[]) {
this.filter.formats = formats.map(item => item) || [];
updateFormatFilters(formats: FilterItem<MangaFormat>[]) {
this.filter.formats = formats.map(item => item.value) || [];
}
updateLibraryFilters(libraries: Library[]) {

View file

@ -1,19 +0,0 @@
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)">
<h2 title>
On Deck
</h2>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout
[isLoading]="isLoading"
[items]="series"
[pagination]="pagination"
[filterSettings]="filterSettings"
[filterOpen]="filterOpen"
(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>

View file

@ -1,133 +0,0 @@
import { Component, EventEmitter, 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 '../metadata-filter/filter-settings';
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();
filterOpen: EventEmitter<boolean> = new EventEmitter();
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;
}
}
}

View file

@ -1,4 +1,4 @@
<app-side-nav-companion-bar [showGoBack]="true">
<app-side-nav-companion-bar>
<h2 title>
<span *ngIf="actions.length > 0">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title"></app-card-actionables>

View file

@ -1,4 +1,4 @@
<app-side-nav-companion-bar [showGoBack]="true" pageHeader="Home">
<app-side-nav-companion-bar pageHeader="Home">
<h2 title>
<app-card-actionables [actions]="actions"></app-card-actionables>
Reading Lists

View file

@ -498,7 +498,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;
}
@ -511,7 +516,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;

View file

@ -6,8 +6,6 @@
<div class="row g-0 mb-2">
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating" a11y-click="13,32" class="clickable col-auto" (click)="goTo('ageRating', seriesMetadata.ageRating)" [selectionMode]="TagBadgeCursor.Clickable">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
<ng-container *ngIf="series">
<!-- Maybe we can put the library this resides in to make it easier to get back -->
<!-- tooltip here explaining how this is year of first issue -->
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date" class="col-auto">{{seriesMetadata.releaseYear}}</app-tag-badge>
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo('languages', seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
<app-tag-badge title="Publication Status" a11y-click="13,32" class="col-auto" (click)="goTo('publicationStatus', seriesMetadata.publicationStatus)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>

View file

@ -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];

View file

@ -10,11 +10,6 @@ import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateR
styleUrls: ['./side-nav-companion-bar.component.scss']
})
export class SideNavCompanionBarComponent implements OnInit {
/**
* Show a dedicated button to go back one history event.
*/
@Input() showGoBack: boolean = false;
/**
* If the page should show a filter
*/

View file

@ -1,14 +1,13 @@
<ng-container>
<div class="side-nav" [ngClass]="{'closed' : !(navService?.sideNavCollapsed$ | async), 'hidden' :!(navService?.sideNavVisibility$ | async)}" *ngIf="accountService.currentUser$ | async as user">
<app-side-nav-item icon="fa-user-circle align-self-center phone-hidden" [title]="user.username | sentenceCase" link="/preferences/">
<!-- <app-side-nav-item icon="fa-user-circle align-self-center phone-hidden" [title]="user.username | sentenceCase" link="/preferences/">
<ng-container actions>
<!-- Todo: This will be customize dashboard/side nav controls-->
Todo: This will be customize dashboard/side nav controls
<a href="/preferences/" title="User Settings"><span class="visually-hidden">User Settings</span></a>
</ng-container>
</app-side-nav-item>
</app-side-nav-item> -->
<div class="mt-3">
<app-side-nav-item icon="fa-home" title="Home" link="/library/"></app-side-nav-item>
<app-side-nav-item icon="fa-home" title="Home" link="/library/"></app-side-nav-item>
<app-side-nav-item icon="fa-list" title="Collections" link="/collections/"></app-side-nav-item>
<app-side-nav-item icon="fa-list-ol" title="Reading Lists" link="/lists/"></app-side-nav-item>
<app-side-nav-item icon="fa-regular fa-rectangle-list" title="All Series" link="/all-series/"></app-side-nav-item>
@ -24,8 +23,7 @@
<ng-container actions>
<app-card-actionables [actions]="actions" [labelBy]="library.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event, library)"></app-card-actionables>
</ng-container>
</app-side-nav-item>
</div>
</app-side-nav-item>
</div>
<div class="side-nav-overlay" [ngClass]="{'closed' : !(navService?.sideNavCollapsed$ | async)}"></div>
<div class="side-nav-overlay" (click)="navService?.toggleSideNav()" [ngClass]="{'closed' : !(navService?.sideNavCollapsed$ | async)}"></div>
</ng-container>

View file

@ -1,5 +1,5 @@
.side-nav {
padding: 10px 0;
padding-bottom: 10px;
width: 190px;
background-color: var(--side-nav-bg-color);
height: calc(100vh - 85px);
@ -24,12 +24,17 @@
background-color: var(--side-nav-closed-bg-color);
border: var(--side-nav-border-closed);
}
.side-nav-item:first() {
border-top-left-radius: var(--side-nav-border-radius);
border-top-right-radius: var(--side-nav-border-radius);
}
}
@media (max-width: 576px) {
.side-nav {
padding: 10px 0;
width: 90vw;
width: 55vw;
background-color: var(--side-nav-mobile-bg-color);
height: calc(100vh - 56px);
position: fixed;
@ -46,6 +51,11 @@
overflow: hidden;
box-shadow: none;
}
.side-nav-item:first() {
border-top-left-radius: var(--side-nav-border-radius);
border-top-right-radius: var(--side-nav-border-radius);
}
}
.side-nav-overlay {

View file

@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { take } from 'rxjs/operators';
import { Observable, Subject } from 'rxjs';
import { take, takeUntil, takeWhile } from 'rxjs/operators';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { UtilityService } from '../../shared/_services/utility.service';
import { Library } from '../../_models/library';
import { User } from '../../_models/user';
@ -15,7 +17,7 @@ import { NavService } from '../../_services/nav.service';
templateUrl: './side-nav.component.html',
styleUrls: ['./side-nav.component.scss']
})
export class SideNavComponent implements OnInit {
export class SideNavComponent implements OnInit, OnDestroy {
user: User | undefined;
libraries: Library[] = [];
@ -27,9 +29,11 @@ export class SideNavComponent implements OnInit {
return library.name.toLowerCase().indexOf((this.filterQuery || '').toLowerCase()) >= 0;
}
private onDestory: Subject<void> = new Subject();
constructor(public accountService: AccountService, private libraryService: LibraryService,
public utilityService: UtilityService, private router: Router,
public utilityService: UtilityService, private messageHub: MessageHubService,
private actionFactoryService: ActionFactoryService, private actionService: ActionService, public navService: NavService) { }
ngOnInit(): void {
@ -44,9 +48,20 @@ export class SideNavComponent implements OnInit {
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
});
this.messageHub.messages$.pipe(takeUntil(this.onDestory), takeWhile(event => event.event === EVENTS.LibraryModified)).subscribe(event => {
this.libraryService.getLibrariesForMember().pipe(take(1)).subscribe((libraries: Library[]) => {
this.libraries = libraries;
});
});
}
ngOnDestroy(): void {
this.onDestory.next();
this.onDestory.complete();
}
handleAction(action: Action, library: Library) {
switch (action) {
case(Action.ScanLibrary):

View file

@ -15,8 +15,8 @@
<div class="spinner-border spinner-border-sm {{settings.multiple ? 'close-offset' : ''}}" role="status" *ngIf="isLoadingOptions">
<span class="visually-hidden">Loading...</span>
</div>
<ng-container *ngIf="settings.multiple">
<button class="btn btn-close float-end mt-2" style="font-size: 0.8rem;" (click)="clearSelections($event)"></button>
<ng-container *ngIf="settings.multiple && (selectedData | async) as selected">
<button class="btn btn-close float-end mt-2" *ngIf="selected.length > 0" style="font-size: 0.8rem;" (click)="clearSelections($event)"></button>
</ng-container>
</div>
</div>

View file

@ -106,4 +106,16 @@ input {
margin: auto;
cursor: pointer;
top: 30%;
}
.results {
.list-group-item {
cursor: pointer;
border-left-color: var(--list-group-hover-text-color);
border-right-color: var(--list-group-hover-text-color);
}
.list-group-item:last-child {
border-bottom-color: var(--list-group-hover-text-color);
}
}

View file

@ -5,8 +5,6 @@ import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap
import { KEY_CODES } from '../shared/_services/utility.service';
import { SelectionCompareFn, TypeaheadSettings } from './typeahead-settings';
//export type SelectionCompareFn<T> = (a: T, b: T) => boolean;
/**
* SelectionModel<T> is used for keeping track of multiple selections. Simple interface with ability to toggle.
* @param selectedState Optional state to set selectedOptions to. If not passed, defaults to false.

View file

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

View file

@ -36,7 +36,8 @@ import { ColorPickerModule } from 'ngx-color-picker';
ColorPickerModule, // User prefernces background color
],
exports: [
SiteThemeProviderPipe
SiteThemeProviderPipe,
ApiKeyComponent
]
})
export class UserSettingsModule { }