Merged develop into main

This commit is contained in:
Joseph Milazzo 2021-11-27 09:46:57 -06:00
commit 8a19c1da9e
103 changed files with 1242 additions and 900 deletions

View file

@ -71,7 +71,7 @@
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
"maximumError": "5kb"
}
]
}

165
UI/Web/package-lock.json generated
View file

@ -89,6 +89,36 @@
"webpack-sources": "2.0.1",
"webpack-subresource-integrity": "1.5.1",
"worker-plugin": "5.0.0"
},
"dependencies": {
"postcss": {
"version": "7.0.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
"integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
"source-map": "^0.6.1",
"supports-color": "^6.1.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"@angular-devkit/build-optimizer": {
@ -3252,9 +3282,9 @@
"dev": true
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles": {
"version": "3.2.1",
@ -4591,9 +4621,9 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"color-string": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
"integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz",
"integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==",
"dev": true,
"requires": {
"color-name": "^1.0.0",
@ -6820,9 +6850,9 @@
}
},
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
@ -9620,9 +9650,9 @@
}
},
"ws": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz",
"integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==",
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
"dev": true
}
}
@ -9710,9 +9740,9 @@
}
},
"jszip": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz",
"integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
"integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
"dev": true,
"requires": {
"lie": "~3.3.0",
@ -11269,20 +11299,6 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
},
"tar": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
"integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==",
"dev": true,
"requires": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^3.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
}
}
}
},
@ -11416,9 +11432,9 @@
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"path-to-regexp": {
"version": "0.1.7",
@ -11451,6 +11467,12 @@
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true
},
"picocolors": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
"integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
"dev": true
},
"picomatch": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
@ -11534,14 +11556,13 @@
"dev": true
},
"postcss": {
"version": "7.0.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
"integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==",
"version": "7.0.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
"integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
"source-map": "^0.6.1",
"supports-color": "^6.1.0"
"picocolors": "^0.2.1",
"source-map": "^0.6.1"
},
"dependencies": {
"source-map": {
@ -11549,15 +11570,6 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
@ -14752,9 +14764,9 @@
"dev": true
},
"tar": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz",
"integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==",
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
"integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
"dev": true,
"requires": {
"chownr": "^2.0.0",
@ -14919,9 +14931,9 @@
}
},
"tmpl": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"dev": true
},
"to-arraybuffer": {
@ -15390,9 +15402,9 @@
}
},
"url-parse": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"requires": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
@ -16678,45 +16690,12 @@
"dev": true
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"dev": true,
"requires": {
"string-width": "^1.0.2 || 2"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dev": true,
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
}
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"wildcard": {

View file

@ -46,10 +46,10 @@ export class ErrorInterceptor implements HttpInterceptor {
}
// If we are not on no-connection, redirect there and save current url so when we refersh, we redirect back there
if (this.router.url !== '/no-connection') {
localStorage.setItem(this.urlKey, this.router.url);
this.router.navigateByUrl('/no-connection');
}
// if (this.router.url !== '/no-connection') {
// localStorage.setItem(this.urlKey, this.router.url);
// this.router.navigateByUrl('/no-connection');
// }
break;
}
return throwError(error);

View file

@ -15,4 +15,5 @@ export interface Chapter {
pagesRead: number; // Attached for the given user when requesting from API
isSpecial: boolean;
title: string;
created: string;
}

View file

@ -1,4 +1,4 @@
export interface ScanLibraryProgressEvent {
export interface ProgressEvent {
libraryId: number;
progress: number;
eventTime: string;

View file

@ -0,0 +1,5 @@
export interface SeriesRemovedEvent {
libraryId: number;
seriesId: number;
seriesName: string;
}

View file

@ -5,4 +5,5 @@ export interface UpdateVersionEvent {
updateTitle: string;
updateUrl: string;
isDocker: boolean;
publishDate: string;
}

View file

@ -1,5 +1,18 @@
export enum PageSplitOption {
/**
* Renders the left side of the image then the right side
*/
SplitLeftToRight = 0,
/**
* Renders the right side of the image then the left side
*/
SplitRightToLeft = 1,
NoSplit = 2
/**
* Don't split and show the image in original size
*/
NoSplit = 2,
/**
* Don't split and scale the image to fit screen space
*/
FitSplit = 3
}

View file

@ -26,5 +26,5 @@ export interface Preferences {
export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}];
export const scalingOptions = [{text: 'Automatic', value: ScalingOption.Automatic}, {text: 'Fit to Height', value: ScalingOption.FitToHeight}, {text: 'Fit to Width', value: ScalingOption.FitToWidth}, {text: 'Original', value: ScalingOption.Original}];
export const pageSplitOptions = [{text: 'Right to Left', value: PageSplitOption.SplitRightToLeft}, {text: 'Left to Right', value: PageSplitOption.SplitLeftToRight}, {text: 'No Split', value: PageSplitOption.NoSplit}];
export const pageSplitOptions = [{text: 'Fit to Screen', value: PageSplitOption.FitSplit}, {text: 'Right to Left', value: PageSplitOption.SplitRightToLeft}, {text: 'Left to Right', value: PageSplitOption.SplitLeftToRight}, {text: 'No Split', value: PageSplitOption.NoSplit}];
export const readingModes = [{text: 'Left to Right', value: READER_MODE.MANGA_LR}, {text: 'Up to Down', value: READER_MODE.MANGA_UD}, {text: 'Webtoon', value: READER_MODE.WEBTOON}];

View file

@ -1,4 +1,5 @@
import { EventEmitter, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
@ -6,7 +7,7 @@ import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { environment } from 'src/environments/environment';
import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component';
import { RefreshMetadataEvent } from '../_models/events/refresh-metadata-event';
import { ScanLibraryProgressEvent } from '../_models/events/scan-library-progress-event';
import { ProgressEvent } from '../_models/events/scan-library-progress-event';
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
import { SeriesAddedEvent } from '../_models/events/series-added-event';
import { User } from '../_models/user';
@ -15,11 +16,15 @@ export enum EVENTS {
UpdateAvailable = 'UpdateAvailable',
ScanSeries = 'ScanSeries',
RefreshMetadata = 'RefreshMetadata',
RefreshMetadataProgress = 'RefreshMetadataProgress',
SeriesAdded = 'SeriesAdded',
SeriesRemoved = 'SeriesRemoved',
ScanLibraryProgress = 'ScanLibraryProgress',
OnlineUsers = 'OnlineUsers',
SeriesAddedToCollection = 'SeriesAddedToCollection',
ScanLibraryError = 'ScanLibraryError'
ScanLibraryError = 'ScanLibraryError',
BackupDatabaseProgress = 'BackupDatabaseProgress',
CleanupProgress = 'CleanupProgress'
}
export interface Message<T> {
@ -42,13 +47,13 @@ export class MessageHubService {
onlineUsers$ = this.onlineUsersSource.asObservable();
public scanSeries: EventEmitter<ScanSeriesEvent> = new EventEmitter<ScanSeriesEvent>();
public scanLibrary: EventEmitter<ScanLibraryProgressEvent> = new EventEmitter<ScanLibraryProgressEvent>();
public scanLibrary: EventEmitter<ProgressEvent> = new EventEmitter<ProgressEvent>(); // TODO: Refactor this name to be generic
public seriesAdded: EventEmitter<SeriesAddedEvent> = new EventEmitter<SeriesAddedEvent>();
public refreshMetadata: EventEmitter<RefreshMetadataEvent> = new EventEmitter<RefreshMetadataEvent>();
isAdmin: boolean = false;
constructor(private modalService: NgbModal, private toastr: ToastrService) {
constructor(private modalService: NgbModal, private toastr: ToastrService, private router: Router) {
}
@ -87,6 +92,27 @@ export class MessageHubService {
this.scanLibrary.emit(resp.body);
});
this.hubConnection.on(EVENTS.BackupDatabaseProgress, resp => {
this.messagesSource.next({
event: EVENTS.BackupDatabaseProgress,
payload: resp.body
});
});
this.hubConnection.on(EVENTS.CleanupProgress, resp => {
this.messagesSource.next({
event: EVENTS.CleanupProgress,
payload: resp.body
});
});
this.hubConnection.on(EVENTS.RefreshMetadataProgress, resp => {
this.messagesSource.next({
event: EVENTS.RefreshMetadataProgress,
payload: resp.body
});
});
this.hubConnection.on(EVENTS.SeriesAddedToCollection, resp => {
this.messagesSource.next({
event: EVENTS.SeriesAddedToCollection,
@ -110,11 +136,19 @@ export class MessageHubService {
payload: resp.body
});
this.seriesAdded.emit(resp.body);
if (this.isAdmin) {
// Don't show the toast when user has reader open
if (this.isAdmin && this.router.url.match(/\d+\/manga|book\/\d+/gi) !== null) {
this.toastr.info('Series ' + (resp.body as SeriesAddedEvent).seriesName + ' added');
}
});
this.hubConnection.on(EVENTS.SeriesRemoved, resp => {
this.messagesSource.next({
event: EVENTS.SeriesRemoved,
payload: resp.body
});
});
this.hubConnection.on(EVENTS.RefreshMetadata, resp => {
this.messagesSource.next({
event: EVENTS.RefreshMetadata,

View file

@ -6,7 +6,6 @@ import { environment } from 'src/environments/environment';
import { Chapter } from '../_models/chapter';
import { CollectionTag } from '../_models/collection-tag';
import { InProgressChapter } from '../_models/in-progress-chapter';
import { MangaFormat } from '../_models/manga-format';
import { PaginatedResult } from '../_models/pagination';
import { Series } from '../_models/series';
import { SeriesFilter } from '../_models/series-filter';
@ -112,13 +111,13 @@ export class SeriesService {
);
}
getInProgress(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
getOnDeck(libraryId: number = 0, pageNum?: number, itemsPerPage?: number, filter?: SeriesFilter) {
const data = this.createSeriesFilter(filter);
let params = new HttpParams();
params = this._addPaginationIfExists(params, pageNum, itemsPerPage);
return this.httpClient.post<Series[]>(this.baseUrl + 'series/in-progress?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
return this.httpClient.post<Series[]>(this.baseUrl + 'series/on-deck?libraryId=' + libraryId, data, {observe: 'response', params}).pipe(
map(response => {
return this._cachePaginatedResults(response, new PaginatedResult<Series[]>());
}));

View file

@ -1,41 +0,0 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import * as Bowser from "bowser";
import { take } from "rxjs/operators";
import { environment } from "src/environments/environment";
import { ClientInfo } from "../_models/stats/client-info";
import { DetailsVersion } from "../_models/stats/details-version";
import { NavService } from "./nav.service";
import { version } from '../../../package.json';
@Injectable({
providedIn: 'root'
})
export class StatsService {
baseUrl = environment.apiUrl;
constructor(private httpClient: HttpClient, private navService: NavService) { }
public sendClientInfo(data: ClientInfo) {
return this.httpClient.post(this.baseUrl + 'stats/client-info', data);
}
public async getInfo(): Promise<ClientInfo> {
const screenResolution = `${window.screen.width} x ${window.screen.height}`;
const browser = Bowser.getParser(window.navigator.userAgent);
const usingDarkTheme = await this.navService.darkMode$.pipe(take(1)).toPromise();
return {
os: browser.getOS() as DetailsVersion,
browser: browser.getBrowser() as DetailsVersion,
platformType: browser.getPlatformType(),
kavitaUiVersion: version,
screenResolution,
usingDarkTheme
};
}
}

View file

@ -6,17 +6,24 @@
</button>
</div>
<div class="modal-body">
<div class="list-group">
<li class="list-group-item" *ngFor="let library of selectedLibraries; let i = index">
<div class="form-check">
<input id="library-{{i}}" type="checkbox" attr.aria-label="Library {{library.data.name}}" class="form-check-input"
[(ngModel)]="library.selected" name="library">
<label attr.for="library-{{i}}" class="form-check-label">{{library.data.name}}</label>
</div>
</li>
<li class="list-group-item" *ngIf="selectedLibraries.length === 0">
There are no libraries setup yet.
</li>
<div class="list-group" *ngIf="!isLoading">
<div class="form-check">
<input id="selectall" type="checkbox" class="form-check-input"
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
<label for="selectall" class="form-check-label">{{selectAll ? 'Deselect' : 'Select'}} All</label>
</div>
<ul>
<li class="list-group-item" *ngFor="let library of allLibraries; let i = index">
<div class="form-check">
<input id="library-{{i}}" type="checkbox" class="form-check-input" attr.aria-label="Library {{library.name}}"
[ngModel]="selections.isSelected(library)" (change)="handleSelection(library)">
<label attr.for="library-{{i}}" class="form-check-label">{{library.name}}</label>
</div>
</li>
<li class="list-group-item" *ngIf="allLibraries.length === 0">
There are no libraries setup yet.
</li>
</ul>
</div>
</div>
<div class="modal-footer">

View file

@ -1,6 +1,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { SelectionModel } from 'src/app/typeahead/typeahead.component';
import { Library } from 'src/app/_models/library';
import { Member } from 'src/app/_models/member';
import { LibraryService } from 'src/app/_services/library.service';
@ -15,24 +16,21 @@ export class LibraryAccessModalComponent implements OnInit {
@Input() member: Member | undefined;
allLibraries: Library[] = [];
selectedLibraries: Array<{selected: boolean, data: Library}> = [];
selections!: SelectionModel<Library>;
selectAll: boolean = false;
isLoading: boolean = false;
get hasSomeSelected() {
console.log(this.selections != null && this.selections.hasSomeSelected());
return this.selections != null && this.selections.hasSomeSelected();
}
constructor(public modal: NgbActiveModal, private libraryService: LibraryService, private fb: FormBuilder) { }
ngOnInit(): void {
this.libraryService.getLibraries().subscribe(libs => {
this.allLibraries = libs;
this.selectedLibraries = libs.map(item => {
return {selected: false, data: item};
});
if (this.member !== undefined) {
this.member.libraries.forEach(lib => {
const foundLibrary = this.selectedLibraries.filter(item => item.data.name === lib.name);
if (foundLibrary.length > 0) {
foundLibrary[0].selected = true;
}
});
}
this.setupSelections();
});
}
@ -45,25 +43,41 @@ export class LibraryAccessModalComponent implements OnInit {
return;
}
const selectedLibraries = this.selectedLibraries.filter(item => item.selected).map(item => item.data);
const selectedLibraries = this.selections.selected();
this.libraryService.updateLibrariesForMember(this.member?.username, selectedLibraries).subscribe(() => {
this.modal.close(true);
});
}
reset() {
this.selectedLibraries = this.allLibraries.map(item => {
return {selected: false, data: item};
});
setupSelections() {
this.selections = new SelectionModel<Library>(false, this.allLibraries);
this.isLoading = false;
// If a member is passed in, then auto-select their libraries
if (this.member !== undefined) {
this.member.libraries.forEach(lib => {
const foundLibrary = this.selectedLibraries.filter(item => item.data.name === lib.name);
if (foundLibrary.length > 0) {
foundLibrary[0].selected = true;
}
this.selections.toggle(lib, true, (a, b) => a.name === b.name);
});
this.selectAll = this.selections.selected().length === this.allLibraries.length;
}
}
reset() {
this.setupSelections();
}
toggleAll() {
this.selectAll = !this.selectAll;
this.allLibraries.forEach(s => this.selections.toggle(s, this.selectAll));
}
handleSelection(item: Library) {
this.selections.toggle(item);
const numberOfSelected = this.selections.selected().length;
if (numberOfSelected == 0) {
this.selectAll = false;
} else if (numberOfSelected == this.selectedLibraries.length) {
this.selectAll = true;
}
}

View file

@ -1,8 +1,9 @@
export interface ServerInfo {
os: string;
dotNetVersion: string;
dotnetVersion: string;
runTimeVersion: string;
kavitaVersion: string;
buildBranch: string;
culture: string;
NumOfCores: number;
installId: string;
isDocker: boolean;
}

View file

@ -1,15 +1,19 @@
<ng-container *ngFor="let update of updates; let indx = index;">
<div class="changelog">
<ng-container *ngFor="let update of updates; let indx = index;">
<div class="card w-100 mb-2" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">{{update.updateTitle}}&nbsp;
<h4 class="card-title">{{update.updateTitle}}&nbsp;
<span class="badge badge-secondary" *ngIf="update.updateVersion === update.currentVersion">Installed</span>
<span class="badge badge-secondary" *ngIf="update.updateVersion > update.currentVersion">Available</span>
</h5>
</h4>
<h6 class="card-subtitle mb-2 text-muted">Published: {{update.publishDate | date: 'short'}}</h6>
<pre class="card-text update-body" [innerHtml]="update.updateBody | safeHtml"></pre>
<a *ngIf="!update.isDocker" href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-right" target="_blank">Download</a>
</div>
</div>
</ng-container>
</ng-container>
</div>
<div class="spinner-border text-secondary" *ngIf="isLoading" role="status">

View file

@ -2,4 +2,20 @@
width: 100%;
word-wrap: break-word;
white-space: pre-wrap;
}
}
::ng-deep .changelog {
h1 {
font-size: 26px;
}
p, ul {
margin-bottom: 0px;
}
}

View file

@ -4,7 +4,7 @@ import { ToastrService } from 'ngx-toastr';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { ConfirmService } from 'src/app/shared/confirm.service';
import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event';
import { ProgressEvent } from 'src/app/_models/events/scan-library-progress-event';
import { Library, LibraryType } from 'src/app/_models/library';
import { LibraryService } from 'src/app/_services/library.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
@ -38,9 +38,9 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
// when a progress event comes in, show it on the UI next to library
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => {
if (event.event != EVENTS.ScanLibraryProgress) return;
if (event.event !== EVENTS.ScanLibraryProgress) return;
const scanEvent = event.payload as ScanLibraryProgressEvent;
const scanEvent = event.payload as ProgressEvent;
this.scanInProgress[scanEvent.libraryId] = {progress: scanEvent.progress !== 100};
if (scanEvent.progress === 0) {
this.scanInProgress[scanEvent.libraryId].timestamp = scanEvent.eventTime;
@ -55,6 +55,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
}
});
}
});
}

View file

@ -33,8 +33,8 @@
<dt>Version</dt>
<dd>{{serverInfo.kavitaVersion}}</dd>
<dt>.NET Version</dt>
<dd>{{serverInfo.dotNetVersion}}</dd>
<dt>Install ID</dt>
<dd>{{serverInfo.installId}}</dd>
</dl>
</div>
@ -43,7 +43,7 @@
<div>
<div class="row">
<div class="col-4">Home page:</div>
<div class="col"><a href="https://kavitareader.com" target="_blank">kavitareader.com</a></div>
<div class="col"><a href="https://www.kavitareader.com" target="_blank">kavitareader.com</a></div>
</div>
<div class="row">
<div class="col-4">Wiki:</div>
@ -63,7 +63,6 @@
</div>
<div class="row">
<div class="col-4">Feature Requests:</div>
<div class="col"><a href="https://feathub.com/Kareadita/Kavita" target="_blank">Feathub</a><br/>
<a href="https://github.com/Kareadita/Kavita/issues" target="_blank">Github issues</a></div>
<div class="col"><a href="https://feats.kavitareader.com" target="_blank">https://feats.kavitareader.com</a><br/>
</div>
</div>

View file

@ -7,7 +7,7 @@ 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 { InProgressComponent } from './in-progress/in-progress.component';
import { OnDeckComponent } from './on-deck/on-deck.component';
import { DashboardComponent } from './dashboard/dashboard.component';
// TODO: Once we modularize the components, use this and measure performance impact: https://angular.io/guide/lazy-loading-ngmodules#preloading-modules
@ -54,7 +54,7 @@ const routes: Routes = [
children: [
{path: 'library', component: DashboardComponent},
{path: 'recently-added', component: RecentlyAddedComponent},
{path: 'in-progress', component: InProgressComponent},
{path: 'on-deck', component: OnDeckComponent},
]
},
{path: 'login', component: UserLoginComponent},

View file

@ -5,7 +5,6 @@ import { AccountService } from './_services/account.service';
import { LibraryService } from './_services/library.service';
import { MessageHubService } from './_services/message-hub.service';
import { NavService } from './_services/nav.service';
import { StatsService } from './_services/stats.service';
import { filter } from 'rxjs/operators';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@ -17,8 +16,8 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
export class AppComponent implements OnInit {
constructor(private accountService: AccountService, public navService: NavService,
private statsService: StatsService, private messageHub: MessageHubService,
private libraryService: LibraryService, private router: Router, private ngbModal: NgbModal) {
private messageHub: MessageHubService, private libraryService: LibraryService,
private router: Router, private ngbModal: NgbModal) {
// Close any open modals when a route change occurs
router.events
@ -32,10 +31,6 @@ export class AppComponent implements OnInit {
ngOnInit(): void {
this.setCurrentUser();
this.statsService.getInfo().then(data => {
this.statsService.sendClientInfo(data).subscribe(() => {/* No Operation */});
});
}

View file

@ -1,13 +1,13 @@
import { BrowserModule, Title } from '@angular/platform-browser';
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
import { NgModule } from '@angular/core';
import { APP_BASE_HREF } from '@angular/common';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbPopoverModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
import { NavHeaderComponent } from './nav-header/nav-header.component';
import { JwtInterceptor } from './_interceptors/jwt.interceptor';
import { UserLoginComponent } from './user-login/user-login.component';
@ -25,13 +25,14 @@ import { CarouselModule } from './carousel/carousel.module';
import { PersonBadgeComponent } from './person-badge/person-badge.component';
import { TypeaheadModule } from './typeahead/typeahead.module';
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
import { InProgressComponent } from './in-progress/in-progress.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';
import { ReadingListModule } from './reading-list/reading-list.module';
import { SAVER, getSaver } from './shared/_providers/saver.provider';
import { ConfigData } from './_models/config-data';
import { NavEventsToggleComponent } from './nav-events-toggle/nav-events-toggle.component';
@NgModule({
@ -46,8 +47,9 @@ import { ConfigData } from './_models/config-data';
ReviewSeriesModalComponent,
PersonBadgeComponent,
RecentlyAddedComponent,
InProgressComponent,
OnDeckComponent,
DashboardComponent,
NavEventsToggleComponent,
],
imports: [
HttpClientModule,
@ -59,6 +61,7 @@ import { ConfigData } from './_models/config-data';
NgbDropdownModule, // Nav
AutocompleteLibModule, // Nav
NgbPopoverModule, // Nav Events toggle
NgbRatingModule, // Series Detail
NgbNavModule,
NgbPaginationModule,

View file

@ -2,7 +2,7 @@
<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>
<app-drawer #commentDrawer="drawer" [isOpen]="drawerOpen" [style.--drawer-width]="'300px'" [options]="{topOffset: topOffset}" [style.--drawer-background-color]="backgroundColor" (drawerClosed)="closeDrawer()">
<app-drawer #commentDrawer="drawer" [isOpen]="drawerOpen" [style.--drawer-width]="'300px'" [options]="{topOffset: topOffset}" [style.--drawer-background-color]="drawerBackgroundColor" (drawerClosed)="closeDrawer()">
<div header>
<h2 style="margin-top: 0.5rem">Book Settings
<button type="button" class="close" aria-label="Close" (click)="commentDrawer.close()">
@ -100,7 +100,7 @@
</div>
<div #readingSection class="reading-section" [ngStyle]="{'padding-top': topOffset + 20 + 'px'}" [@isLoading]="isLoading ? true : false" (click)="handleReaderClick($event)">
<div #readingHtml class="book-content" [ngStyle]="{'padding-bottom': topOffset + 20 + 'px'}" [innerHtml]="page" *ngIf="page !== undefined"></div>
<div #readingHtml class="book-content" [ngStyle]="{'padding-bottom': topOffset + 20 + 'px', 'margin': '0px 0px'}" [innerHtml]="page" *ngIf="page !== undefined"></div>
<div class="left {{clickOverlayClass('left')}} no-observe" (click)="prevPage()" *ngIf="clickToPaginate">
</div>

View file

@ -229,6 +229,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
}
get drawerBackgroundColor() {
return this.darkMode ? '#010409': '#fff';
}
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
private seriesService: SeriesService, private readerService: ReaderService, private location: Location,
private renderer: Renderer2, private navService: NavService, private toastr: ToastrService,
@ -887,7 +891,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
getDarkModeBackgroundColor() {
return this.darkMode ? '#010409' : '#fff';
return this.darkMode ? '#292929' : '#fff';
}
setOverrideStyles() {

View file

@ -17,11 +17,12 @@
<div class="col">
Id: {{data.id}}
</div>
<div class="col">
<div class="col" *ngIf="series !== undefined">
Format: <span class="badge badge-secondary">{{utilityService.mangaFormat(series.format) | sentenceCase}}</span>
</div>
</div>
<div class="row no-gutters">
<div class="col" *ngIf="utilityService.isVolume(data)">
<div class="col" *ngIf="data.hasOwnProperty('created')">
Added: {{(data.created | date: 'short') || '-'}}
</div>
<div class="col">
@ -58,8 +59,8 @@
<div class="col">
Pages: {{file.pages}}
</div>
<div class="col">
Format: <span class="badge badge-secondary">{{utilityService.mangaFormatToText(file.format)}}</span>
<div class="col" *ngIf="data.hasOwnProperty('created')">
Added: {{(data.created | date: 'short') || '-'}}
</div>
</div>
</li>

View file

@ -15,6 +15,8 @@ import { UploadService } from 'src/app/_services/upload.service';
import { ChangeCoverImageModalComponent } from '../change-cover-image/change-cover-image-modal.component';
import { LibraryType } from '../../../_models/library';
import { LibraryService } from '../../../_services/library.service';
import { SeriesService } from 'src/app/_services/series.service';
import { Series } from 'src/app/_models/series';
@ -42,6 +44,7 @@ export class CardDetailsModalComponent implements OnInit {
actions: ActionItem<any>[] = [];
chapterActions: ActionItem<Chapter>[] = [];
libraryType: LibraryType = LibraryType.Manga;
series: Series | undefined = undefined;
get LibraryType(): typeof LibraryType {
return LibraryType;
@ -50,7 +53,8 @@ export class CardDetailsModalComponent implements OnInit {
constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService,
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService,
private accountService: AccountService, private actionFactoryService: ActionFactoryService,
private actionService: ActionService, private router: Router, private libraryService: LibraryService) { }
private actionService: ActionService, private router: Router, private libraryService: LibraryService,
private seriesService: SeriesService) { }
ngOnInit(): void {
this.isChapter = this.utilityService.isChapter(this.data);
@ -79,6 +83,10 @@ export class CardDetailsModalComponent implements OnInit {
this.chapters.forEach((c: Chapter) => {
c.files.sort((a: MangaFile, b: MangaFile) => collator.compare(a.filePath, b.filePath));
});
this.seriesService.getSeries(this.seriesId).subscribe(series => {
this.series = series;
})
}
close() {

View file

@ -26,7 +26,7 @@
<h6>Applies to Series</h6>
<div class="form-check">
<input id="selectall" type="checkbox" class="form-check-input"
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="someSelected">
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
<label for="selectall" class="form-check-label">{{selectAll ? 'Deselect' : 'Select'}} All</label>
</div>
<ul>

View file

@ -35,6 +35,11 @@ export class EditCollectionTagsComponent implements OnInit {
imageUrls: Array<string> = [];
selectedCover: string = '';
get hasSomeSelected() {
return this.selections != null && this.selections.hasSomeSelected();
}
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
private collectionService: CollectionTagService, private toastr: ToastrService,
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
@ -133,11 +138,6 @@ export class EditCollectionTagsComponent implements OnInit {
});
}
get someSelected() {
const selected = this.selections.selected();
return (selected.length !== this.series.length && selected.length !== 0);
}
updateSelectedIndex(index: number) {
this.collectionTagForm.patchValue({
coverImageIndex: index

View file

@ -110,7 +110,7 @@
<div>
<div class="row no-gutters">
<div class="col">
Created: {{volume.created | date: 'short'}}
Added: {{volume.created | date: 'short'}}
</div>
<div class="col">
Last Modified: {{volume.lastModified | date: 'short'}}

View file

@ -1,9 +1,8 @@
@import "../../../theme/colors";
@import "../../../assets/themes/dark.scss";
@use "../../../theme/colors";
.bulk-select {
background-color: $dark-form-background-no-opacity;
border-bottom: 2px solid $primary-color;
background-color: colors.$dark-form-background-no-opacity;
border-bottom: 2px solid colors.$primary-color;
color: white;
}
@ -12,5 +11,5 @@
}
.highlight {
color: $primary-color !important;
color: colors.$primary-color !important;
}

View file

@ -1,4 +1,4 @@
@import '../../../theme/colors';
@use '../../../theme/colors';
$image-height: 230px;
$image-width: 160px;
@ -14,7 +14,7 @@ $image-width: 160px;
}
.selected {
outline: 5px solid $primary-color;
outline: 5px solid colors.$primary-color;
outline-width: medium;
outline-offset: -1px;
}
@ -22,7 +22,7 @@ $image-width: 160px;
ngx-file-drop ::ng-deep > div {
// styling for the outer drop box
width: 100%;
border: 2px solid $primary-color;
border: 2px solid colors.$primary-color;
border-radius: 5px;
height: 100px;
margin: auto;

View file

@ -11,6 +11,7 @@ import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collecti
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { CollectionTag } from 'src/app/_models/collection-tag';
import { SeriesAddedToCollectionEvent } from 'src/app/_models/events/series-added-to-collection-event';
import { SeriesRemovedEvent } from 'src/app/_models/events/series-removed-event';
import { Pagination } from 'src/app/_models/pagination';
import { Series } from 'src/app/_models/series';
import { FilterItem, mangaFormatFilters, SeriesFilter } from 'src/app/_models/series-filter';
@ -106,8 +107,12 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this));
this.messageHub.messages$.pipe(takeWhile(event => event.event === EVENTS.SeriesAddedToCollection), takeUntil(this.onDestory), debounceTime(2000)).subscribe(event => {
const collectionEvent = event.payload as SeriesAddedToCollectionEvent;
if (collectionEvent.tagId === this.collectionTag.id) {
if (event.event == EVENTS.SeriesAddedToCollection) {
const collectionEvent = event.payload as SeriesAddedToCollectionEvent;
if (collectionEvent.tagId === this.collectionTag.id) {
this.loadPage();
}
} else if (event.event === EVENTS.SeriesRemoved) {
this.loadPage();
}
});

View file

@ -5,7 +5,7 @@
<p>You haven't been granted access to any libraries.</p>
</div>
<app-carousel-reel [items]="inProgress" title="In Progress" (sectionClick)="handleSectionClick($event)">
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
<ng-template #carouselItem let-item let-position="idx">
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
</ng-template>

View file

@ -1,19 +1,15 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { EditCollectionTagsComponent } from '../cards/_modals/edit-collection-tags/edit-collection-tags.component';
import { CollectionTag } from '../_models/collection-tag';
import { SeriesAddedEvent } from '../_models/events/series-added-event';
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
import { InProgressChapter } from '../_models/in-progress-chapter';
import { Library } from '../_models/library';
import { Series } from '../_models/series';
import { User } from '../_models/user';
import { AccountService } from '../_services/account.service';
import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service';
import { CollectionTagService } from '../_services/collection-tag.service';
import { ImageService } from '../_services/image.service';
import { LibraryService } from '../_services/library.service';
import { EVENTS, MessageHubService } from '../_services/message-hub.service';
@ -44,14 +40,18 @@ export class LibraryComponent implements OnInit, OnDestroy {
private titleService: Title, public imageService: ImageService,
private messageHub: MessageHubService) {
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
if (res.event == EVENTS.SeriesAdded) {
if (res.event === EVENTS.SeriesAdded) {
const seriesAddedEvent = res.payload as SeriesAddedEvent;
this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
this.recentlyAdded.unshift(series);
});
} else if (res.event === EVENTS.SeriesRemoved) {
const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
this.recentlyAdded = this.recentlyAdded.filter(item => item.id != seriesRemovedEvent.seriesId);
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
}
});
}
}
ngOnInit(): void {
this.titleService.setTitle('Kavita - Dashboard');
@ -75,7 +75,7 @@ export class LibraryComponent implements OnInit, OnDestroy {
reloadSeries() {
this.loadRecentlyAdded();
this.loadInProgress();
this.loadOnDeck();
}
reloadInProgress(series: Series | boolean) {
@ -88,11 +88,11 @@ export class LibraryComponent implements OnInit, OnDestroy {
return;
}
this.loadInProgress();
this.loadOnDeck();
}
loadInProgress() {
this.seriesService.getInProgress().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
loadOnDeck() {
this.seriesService.getOnDeck().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
this.inProgress = updatedSeries.result;
});
}
@ -108,8 +108,15 @@ export class LibraryComponent implements OnInit, OnDestroy {
this.router.navigate(['collections']);
} else if (sectionTitle.toLowerCase() === 'recently added') {
this.router.navigate(['recently-added']);
} else if (sectionTitle.toLowerCase() === 'in progress') {
this.router.navigate(['in-progress']);
} else if (sectionTitle.toLowerCase() === 'on deck') {
this.router.navigate(['on-deck']);
}
}
removeFromArray(arr: Array<any>, element: any) {
const index = arr.indexOf(element);
if (index >= 0) {
arr.splice(index);
}
}
}

View file

@ -26,9 +26,9 @@
</div>
</div>
<ng-container *ngFor="let item of webtoonImages | async; let index = index;">
<img src="{{item.src}}" style="display: block" class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}}" *ngIf="pageNum >= pageNum - bufferPages && pageNum <= pageNum + bufferPages" rel="nofollow" alt="image" (load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">
<img src="{{item.src}}" style="display: block" class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}}" *ngIf="pageNum >= pageNum - bufferPages && pageNum <= pageNum + bufferPages" rel="nofollow" alt="image" (load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">
</ng-container>
<div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadPrevChapter.emit()">
<div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadNextChapter.emit()">
<div>
<button class="btn btn-icon mx-auto">
<i class="fa fa-angle-double-down animate" aria-hidden="true"></i>

View file

@ -21,6 +21,11 @@
}
}
.full-width {
width: 100% !important;
}
@keyframes move-up-down {
0%, 100% {
transform: translateY(0);
@ -28,4 +33,17 @@
50% {
transform: translateY(-10px);
}
}
.bookmark-effect {
animation: bookmark 1s cubic-bezier(0.165, 0.84, 0.44, 1);
}
@keyframes bookmark {
0%, 100% {
filter: opacity(1);
}
50% {
filter: opacity(0.25);
}
}

View file

@ -1,5 +1,4 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { BehaviorSubject, fromEvent, ReplaySubject, Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { ReaderService } from '../../_services/reader.service';
@ -63,6 +62,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
@Output() loadPrevChapter: EventEmitter<void> = new EventEmitter<void>();
@Input() goToPage: ReplaySubject<number> = new ReplaySubject<number>();
@Input() bookmarkPage: ReplaySubject<number> = new ReplaySubject<number>();
/**
* Stores and emits all the src urls
@ -127,12 +127,16 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
return Math.max(...Object.values(this.imagesLoaded));
}
get areImagesWiderThanWindow() {
return this.webtoonImageWidth > (window.innerWidth || document.documentElement.clientWidth);
}
private readonly onDestroy = new Subject<void>();
constructor(private readerService: ReaderService, private renderer: Renderer2, private toastr: ToastrService) {}
constructor(private readerService: ReaderService, private renderer: Renderer2) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.hasOwnProperty('totalPages') && changes['totalPages'].previousValue != changes['totalPages'].currentValue) {
@ -167,11 +171,23 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.setPageNum(page, true);
});
}
if (this.bookmarkPage) {
this.bookmarkPage.pipe(takeUntil(this.onDestroy)).subscribe(page => {
const image = document.querySelector('img[id^="page-' + page + '"]');
if (image) {
this.renderer.addClass(image, 'bookmark-effect');
setTimeout(() => {
this.renderer.removeClass(image, 'bookmark-effect');
}, 1000);
}
});
}
}
/**
* On scroll in document, calculate if the user/javascript has scrolled to the current image element (and it's visible), update that scrolling has ended completely,
* and calculate the direction the scrolling is occuring. This is used for prefetching.
* and calculate the direction the scrolling is occuring. This is not used for prefetching.
* @param event Scroll Event
*/
handleScrollEvent(event?: any) {
@ -179,11 +195,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|| document.documentElement.scrollTop
|| document.body.scrollTop || 0);
if (this.isScrolling && this.currentPageElem != null && this.isElementVisible(this.currentPageElem)) {
this.debugLog('[Scroll] Image is visible from scroll, isScrolling is now false');
this.isScrolling = false;
}
if (verticalOffset > this.prevScrollPosition) {
this.scrollingDirection = PAGING_DIRECTION.FORWARD;
} else {
@ -191,6 +202,23 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
}
this.prevScrollPosition = verticalOffset;
if (this.isScrolling && this.currentPageElem != null && this.isElementVisible(this.currentPageElem)) {
this.debugLog('[Scroll] Image is visible from scroll, isScrolling is now false');
this.isScrolling = false;
}
if (!this.isScrolling) {
// Use offset of the image against the scroll container to test if the most of the image is visible on the screen. We can use this
// to mark the current page and separate the prefetching code.
const midlineImages = Array.from(document.querySelectorAll('img[id^="page-"]'))
.filter(entry => this.shouldElementCountAsCurrentPage(entry));
if (midlineImages.length > 0) {
this.setPageNum(parseInt(midlineImages[0].getAttribute('page') || this.pageNum + '', 10));
}
}
// Check if we hit the last page
this.checkIfShouldTriggerContinuousReader();
@ -219,12 +247,13 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
if (this.atTop && this.pageNum > 0) {
this.atTop = false;
}
// debug mode will add an extra pixel from the image border + (this.debug ? 1 : 0)
if (totalScroll === totalHeight && !this.atBottom) {
this.atBottom = true;
this.setPageNum(this.totalPages);
// Scroll user back to original location
this.previousScrollHeightMinusTop = document.documentElement.scrollTop;
this.previousScrollHeightMinusTop = this.getScrollTop();
requestAnimationFrame(() => document.documentElement.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2));
} else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) {
// This if statement will fire once we scroll into the spacer at all
@ -266,6 +295,28 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
);
}
/**
* Is any part of the element visible in the scrollport and is it above the midline trigger.
* The midline trigger does not mean it is half of the screen. It may be top 25%.
* @param elem HTML Element
* @returns If above midline
*/
shouldElementCountAsCurrentPage(elem: Element) {
if (elem === null || elem === undefined) { return false; }
var rect = elem.getBoundingClientRect();
if (rect.bottom >= 0 &&
rect.right >= 0 &&
rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.left <= (window.innerWidth || document.documentElement.clientWidth)
) {
const topX = (window.innerHeight || document.documentElement.clientHeight);
return Math.abs(rect.top / topX) <= 0.25;
}
return false;
}
initWebtoonReader() {
this.imagesLoaded = {};
@ -327,7 +378,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
this.debugLog('[Intersection] Page ' + imagePage + ' is visible: ', entry.isIntersecting);
if (entry.isIntersecting) {
this.debugLog('[Intersection] ! Page ' + imagePage + ' just entered screen');
this.setPageNum(imagePage);
this.prefetchWebtoonImages(imagePage);
}
});
}
@ -350,10 +401,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
if (scrollToPage) {
const currentImage = document.querySelector('img#page-' + this.pageNum);
if (currentImage !== null && !this.isElementVisible(currentImage)) {
this.debugLog('[GoToPage] Scrolling to page', this.pageNum);
this.scrollToCurrentPage();
}
if (currentImage === null) return;
this.debugLog('[GoToPage] Scrolling to page', this.pageNum);
this.scrollToCurrentPage();
}
}
@ -365,6 +415,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
* Performs the scroll for the current page element. Updates any state variables needed.
*/
scrollToCurrentPage() {
this.debugLog('Scrolling to ', this.pageNum);
this.currentPageElem = document.querySelector('img#page-' + this.pageNum);
if (!this.currentPageElem) { return; }
@ -414,19 +465,29 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
}
}
calculatePrefetchIndecies() {
/**
* Finds the ranges of indecies to load from backend. totalPages - 1 is due to backend will automatically return last page for any page number
* above totalPages. Webtoon reader might ask for that which results in duplicate last pages.
* @param pageNum
* @returns
*/
calculatePrefetchIndecies(pageNum: number = -1) {
if (pageNum == -1) {
pageNum = this.pageNum;
}
let startingIndex = 0;
let endingIndex = 0;
if (this.isScrollingForwards()) {
startingIndex = Math.min(Math.max(this.pageNum - this.bufferPages, 0), this.totalPages);
endingIndex = Math.min(Math.max(this.pageNum + this.bufferPages, 0), this.totalPages);
startingIndex = Math.min(Math.max(pageNum - this.bufferPages, 0), this.totalPages - 1);
endingIndex = Math.min(Math.max(pageNum + this.bufferPages, 0), this.totalPages - 1);
if (startingIndex === this.totalPages) {
return [0, 0];
}
} else {
startingIndex = Math.min(Math.max(this.pageNum - this.bufferPages, 0), this.totalPages);
endingIndex = Math.min(Math.max(this.pageNum + this.bufferPages, 0), this.totalPages);
startingIndex = Math.min(Math.max(pageNum - this.bufferPages, 0), this.totalPages - 1);
endingIndex = Math.min(Math.max(pageNum + this.bufferPages, 0), this.totalPages - 1);
}
@ -443,8 +504,13 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
return [...Array(size).keys()].map(i => i + startAt);
}
prefetchWebtoonImages() {
const [startingIndex, endingIndex] = this.calculatePrefetchIndecies();
prefetchWebtoonImages(pageNum: number = -1) {
if (pageNum === -1) {
pageNum = this.pageNum;
}
const [startingIndex, endingIndex] = this.calculatePrefetchIndecies(pageNum);
if (startingIndex === 0 && endingIndex === 0) { return; }
this.debugLog('\t[PREFETCH] prefetching pages: ' + startingIndex + ' to ' + endingIndex);

View file

@ -24,11 +24,19 @@
</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()}} {{this.colorMode}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} "
ondragstart="return false;" onselectstart="return false;">
</canvas>
<div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading">
<app-infinite-scroller [pageNum]="pageNum" [bufferPages]="5" [goToPage]="goToPageEvent" (pageNumberChange)="handleWebtoonPageChange($event)" [totalPages]="maxPages - 1" [urlProvider]="getPageUrl" (loadNextChapter)="loadNextChapter()" (loadPrevChapter)="loadPrevChapter()"></app-infinite-scroller>
<app-infinite-scroller [pageNum]="pageNum"
[bufferPages]="5"
[goToPage]="goToPageEvent"
(pageNumberChange)="handleWebtoonPageChange($event)"
[totalPages]="maxPages"
[urlProvider]="getPageUrl"
(loadNextChapter)="loadNextChapter()"
(loadPrevChapter)="loadPrevChapter()"
[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>
@ -43,7 +51,7 @@
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter();resetMenuCloseTimer();" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
<button class="btn btn-small btn-icon col-1" [disabled]="prevPageDisabled || pageNum === 0" (click)="goToPage(0);resetMenuCloseTimer();" title="First Page"><i class="fa fa-step-backward" aria-hidden="true"></i></button>
<div class="col custom-slider" *ngIf="pageOptions.ceil > 0; else noSlider">
<ngx-slider [options]="pageOptions" [value]="pageNum" aria-describedby="slider-info" [manualRefresh]="refreshSlider" (userChangeEnd)="sliderPageUpdate($event);startMenuCloseTimer()" (userChangeStart)="cancelMenuCloseTimer();"></ngx-slider>
<ngx-slider [options]="pageOptions" [value]="pageNum" aria-describedby="slider-info" [manualRefresh]="refreshSlider" (userChangeEnd)="sliderPageUpdate($event);startMenuCloseTimer()" (userChange)="sliderDragUpdate($event)" (userChangeStart)="cancelMenuCloseTimer();"></ngx-slider>
</div>
<ng-template #noSlider>
<div class="col custom-slider">
@ -94,9 +102,7 @@
<div class="col-6">
<div class="form-group">
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
<option [value]="1">Right to Left</option>
<option [value]="0">Left to Right</option>
<option [value]="2">None</option>
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text}}</option>
</select>
</div>
</div>

View file

@ -1,4 +1,4 @@
@import '../../theme/colors';
@use '../../theme/colors';
$center-width: 50%;
$side-width: 25%;
@ -178,7 +178,7 @@ canvas {
height: 2px;
}
.custom-slider .ngx-slider .ngx-slider-selection {
background: $primary-color;
background: colors.$primary-color;
}
.custom-slider .ngx-slider .ngx-slider-pointer {
@ -186,7 +186,7 @@ canvas {
height: 16px;
top: auto; /* to remove the default positioning */
bottom: 0;
background-color: $primary-color; // #333;
background-color: colors.$primary-color; // #333;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
@ -217,12 +217,13 @@ canvas {
}
.custom-slider .ngx-slider .ngx-slider-tick.ngx-slider-selected {
background: $primary-color;
background: colors.$primary-color;
}
}
.webtoon-images {
text-align: center;
width: 100%;
}
.highlight {
@ -234,7 +235,16 @@ canvas {
animation: fadein .5s both;
}
// DEBUG
.active-image {
border: 5px solid red;
}
.bookmark-effect {
animation: bookmark 0.7s cubic-bezier(0.165, 0.84, 0.44, 1);
}
@keyframes bookmark {
0%, 100% {
border: 0px;
}
50% {
border: 5px solid colors.$primary-color;
}
}

View file

@ -1,4 +1,4 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { Location } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { take, takeUntil } from 'rxjs/operators';
@ -12,7 +12,7 @@ import { ScalingOption } from '../_models/preferences/scaling-option';
import { PageSplitOption } from '../_models/preferences/page-split-option';
import { forkJoin, ReplaySubject, Subject } from 'rxjs';
import { ToastrService } from 'ngx-toastr';
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
import { KEY_CODES, UtilityService, Breakpoint } from '../shared/_services/utility.service';
import { CircularArray } from '../shared/data-structures/circular-array';
import { MemberService } from '../_services/member.service';
import { Stack } from '../shared/data-structures/stack';
@ -20,7 +20,7 @@ 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 { Preferences, scalingOptions } from '../_models/preferences/preferences';
import { pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
import { READER_MODE } from '../_models/preferences/reader-mode';
import { MangaFormat } from '../_models/manga-format';
import { LibraryService } from '../_services/library.service';
@ -96,12 +96,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
scalingOptions = scalingOptions;
readingDirection = ReadingDirection.LeftToRight;
scalingOption = ScalingOption.FitToHeight;
pageSplitOption = PageSplitOption.SplitRightToLeft;
pageSplitOption = PageSplitOption.FitSplit;
currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT;
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
colorMode: COLOR_FILTER = COLOR_FILTER.NONE;
autoCloseMenu: boolean = true;
readerMode: READER_MODE = READER_MODE.MANGA_LR;
pageSplitOptions = pageSplitOptions;
isLoading = true;
@ -124,7 +126,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* An event emiter when a page change occurs. Used soley by the webtoon reader.
*/
goToPageEvent: ReplaySubject<number> = new ReplaySubject<number>();
/**
* An event emiter when a bookmark on a page change occurs. Used soley by the webtoon reader.
*/
showBookmarkEffectEvent: ReplaySubject<number> = new ReplaySubject<number>();
/**
* If the menu is open/visible.
*/
@ -263,11 +268,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return ReadingDirection;
}
get PageSplitOption(): typeof PageSplitOption {
return PageSplitOption;
}
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
public readerService: ReaderService, private location: Location,
private formBuilder: FormBuilder, private navService: NavService,
private toastr: ToastrService, private memberService: MemberService,
private libraryService: LibraryService, private utilityService: UtilityService) {
private libraryService: LibraryService, private utilityService: UtilityService,
private renderer: Renderer2) {
this.navService.hideNavBar();
}
@ -309,7 +319,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm = this.formBuilder.group({
autoCloseMenu: this.autoCloseMenu,
pageSplitOption: this.pageSplitOption + '',
pageSplitOption: this.pageSplitOption,
fittingOption: this.translateScalingOption(this.scalingOption)
});
@ -317,8 +327,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => {
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
// On change of splitting, re-render the page if the page is already split
const needsSplitting = this.canvasImage.width > this.canvasImage.height;
const needsSplitting = this.isCoverImage();
// If we need to split on a menu change, then we need to re-render.
if (needsSplitting) {
this.loadPage();
}
@ -354,6 +364,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.onDestroy.next();
this.onDestroy.complete();
this.goToPageEvent.complete();
this.showBookmarkEffectEvent.complete();
}
@HostListener('window:keyup', ['$event'])
@ -421,7 +432,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.maxPages = results.chapterInfo.pages;
let page = results.progress.pageNum;
if (page > this.maxPages) {
page = this.maxPages - 1;
page = this.maxPages;
}
this.setPageNum(page);
@ -614,15 +625,20 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
isSplitLeftToRight() {
return (this.generalSettingsForm?.get('pageSplitOption')?.value + '') === (PageSplitOption.SplitLeftToRight + '');
return parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) === PageSplitOption.SplitLeftToRight;
}
/**
*
* @returns If the current model reflects no split of fit split
*/
isNoSplit() {
return (this.generalSettingsForm?.get('pageSplitOption')?.value + '') === (PageSplitOption.NoSplit + '');
const splitValue = parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10);
return splitValue === PageSplitOption.NoSplit || splitValue === PageSplitOption.FitSplit;
}
updateSplitPage() {
const needsSplitting = this.canvasImage.width > this.canvasImage.height;
const needsSplitting = this.isCoverImage();
if (!needsSplitting || this.isNoSplit()) {
this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT;
return;
@ -734,6 +750,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
loadNextChapter() {
if (this.nextPageDisabled) { return; }
if (this.nextChapterDisabled) { return; }
this.isLoading = true;
if (this.nextChapterId === CHAPTER_ID_NOT_FETCHED || this.nextChapterId === this.chapterId) {
this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
@ -747,6 +764,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
loadPrevChapter() {
if (this.prevPageDisabled) { return; }
if (this.prevChapterDisabled) { return; }
this.isLoading = true;
this.continuousChaptersStack.pop();
const prevChapter = this.continuousChaptersStack.peek();
@ -814,21 +832,23 @@ 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;
}
}
return true;
}
renderPage() {
if (this.ctx && this.canvas) {
this.canvasImage.onload = null;
if (!this.setCanvasSize()) return;
this.setCanvasSize();
const needsSplitting = this.canvasImage.width > this.canvasImage.height;
const needsSplitting = this.isCoverImage();
this.updateSplitPage();
if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) {
@ -839,31 +859,39 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height);
} else {
if (!this.firstPageRendered && this.scalingOption === ScalingOption.Automatic) {
let newScale = this.generalSettingsForm.get('fittingOption')?.value;
this.updateScalingForFirstPageRender();
}
// Fit Split on a page that needs splitting
if (this.shouldRenderAsFitSplit()) {
const windowWidth = window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
const windowHeight = window.innerHeight
|| document.documentElement.clientHeight
|| document.body.clientHeight;
const widthRatio = windowWidth / this.canvasImage.width;
const heightRatio = windowHeight / this.canvasImage.height;
// Given that we now have image dimensions, assuming this isn't a split image,
// Try to reset one time based on who's dimension (width/height) is smaller
if (widthRatio < heightRatio) {
newScale = FITTING_OPTION.WIDTH;
} else if (widthRatio > heightRatio) {
newScale = FITTING_OPTION.HEIGHT;
|| document.documentElement.clientHeight
|| document.body.clientHeight;
// If the user's screen is wider than the image, just pretend this is no split, as it will render nicer
this.canvas.nativeElement.width = windowWidth;
this.canvas.nativeElement.height = windowHeight;
const ratio = this.canvasImage.width / this.canvasImage.height;
let newWidth = windowWidth;
let newHeight = newWidth / ratio;
if (newHeight > windowHeight) {
newHeight = windowHeight;
newWidth = newHeight * ratio;
}
this.generalSettingsForm.get('fittingOption')?.setValue(newScale);
this.firstPageRendered = true;
// Optimization: When the screen is larger than newWidth, allow no split rendering to occur for a better fit
if (windowWidth > newWidth) {
this.ctx.drawImage(this.canvasImage, 0, 0);
} else {
this.ctx.drawImage(this.canvasImage, 0, 0, newWidth, newHeight);
}
} else {
this.ctx.drawImage(this.canvasImage, 0, 0);
}
this.ctx.drawImage(this.canvasImage, 0, 0);
}
// Reset scroll on non HEIGHT Fits
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
window.scrollTo(0, 0);
@ -873,6 +901,41 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.isLoading = false;
}
updateScalingForFirstPageRender() {
const windowWidth = window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
const windowHeight = window.innerHeight
|| document.documentElement.clientHeight
|| document.body.clientHeight;
const needsSplitting = this.isCoverImage();
let newScale = this.generalSettingsForm.get('fittingOption')?.value;
const widthRatio = windowWidth / (this.canvasImage.width / (needsSplitting ? 2 : 1));
const heightRatio = windowHeight / (this.canvasImage.height);
// Given that we now have image dimensions, assuming this isn't a split image,
// Try to reset one time based on who's dimension (width/height) is smaller
if (widthRatio < heightRatio) {
newScale = FITTING_OPTION.WIDTH;
} else if (widthRatio > heightRatio) {
newScale = FITTING_OPTION.HEIGHT;
}
this.firstPageRendered = true;
this.generalSettingsForm.get('fittingOption')?.setValue(newScale, {emitEvent: false});
}
isCoverImage() {
return this.canvasImage.width > this.canvasImage.height;
}
shouldRenderAsFitSplit() {
if (!this.isCoverImage() || parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) !== PageSplitOption.FitSplit) return false;
return true;
}
prefetch() {
let index = 1;
@ -895,16 +958,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
loadPage() {
if (!this.canvas || !this.ctx) { return; }
// Due to the fact that we start at image 0, but page 1, we need the last page to have progress as page + 1 to be completed
let pageNum = this.pageNum;
if (this.pageNum == this.maxPages - 1) {
pageNum = this.pageNum + 1;
}
if (!this.incognitoMode) {
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
}
this.isLoading = true;
this.canvasImage = this.cachedImages.current();
if (this.readerService.imageUrlToPageNum(this.canvasImage.src) !== this.pageNum || this.canvasImage.src === '' || !this.canvasImage.complete) {
@ -942,6 +995,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
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
// responds to page changes
if (this.readerMode !== READER_MODE.WEBTOON) {
this.setPageNum(context.value);
}
}
sliderPageUpdate(context: ChangeContext) {
const page = context.value;
@ -974,6 +1035,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
}
// Due to the fact that we start at image 0, but page 1, we need the last page to have progress as page + 1 to be completed
let tempPageNum = this.pageNum;
if (this.pageNum == this.maxPages - 1) {
tempPageNum = this.pageNum + 1;
}
if (!this.incognitoMode) {
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, tempPageNum).pipe(take(1)).subscribe(() => {/* No operation */});
}
}
goToPage(pageNum: number) {
@ -1053,54 +1124,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
handleWebtoonPageChange(updatedPageNum: number) {
this.setPageNum(updatedPageNum);
if (this.incognitoMode) return;
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
}
saveSettings() {
// NOTE: This is not called anywhere
if (this.user === undefined) return;
const data: Preferences = {
readingDirection: this.readingDirection,
scalingOption: this.scalingOption,
pageSplitOption: this.pageSplitOption,
autoCloseMenu: this.autoCloseMenu,
readerMode: this.readerMode,
bookReaderDarkMode: this.user.preferences.bookReaderDarkMode,
bookReaderFontFamily: this.user.preferences.bookReaderFontFamily,
bookReaderFontSize: this.user.preferences.bookReaderFontSize,
bookReaderLineSpacing: this.user.preferences.bookReaderLineSpacing,
bookReaderMargin: this.user.preferences.bookReaderMargin,
bookReaderTapToPaginate: this.user.preferences.bookReaderTapToPaginate,
bookReaderReadingDirection: this.readingDirection,
siteDarkMode: this.user.preferences.siteDarkMode,
};
this.accountService.updatePreferences(data).pipe(take(1)).subscribe((updatedPrefs) => {
this.toastr.success('User settings updated');
if (this.user) {
this.user.preferences = updatedPrefs;
}
this.resetSettings();
});
}
resetSettings() {
this.generalSettingsForm.get('fittingOption')?.value.get('fittingOption')?.setValue(this.translateScalingOption(this.user.preferences.scalingOption));
this.generalSettingsForm.get('pageSplitOption')?.setValue(this.user.preferences.pageSplitOption + '');
this.generalSettingsForm.get('autoCloseMenu')?.setValue(this.autoCloseMenu);
this.updateForm();
}
/**
* Bookmarks the current page for the chapter
*/
bookmarkPage() {
// TODO: Show some sort of UI visual to show that a page was bookmarked
const pageNum = this.pageNum;
if (this.pageBookmarked) {
this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum).pipe(take(1)).subscribe(() => {
@ -1111,7 +1140,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.bookmarks[pageNum] = 1;
});
}
// Show an effect on the image to show that it was bookmarked
this.showBookmarkEffectEvent.next(pageNum);
if (this.readerMode != READER_MODE.WEBTOON) {
if (this.canvas) {
this.renderer.addClass(this.canvas?.nativeElement, 'bookmark-effect');
setTimeout(() => {
this.renderer.removeClass(this.canvas?.nativeElement, 'bookmark-effect');
}, 1000);
}
}
}
/**

View file

@ -0,0 +1,22 @@
<ng-container>
<button type="button" class="btn btn-icon {{progressEventsSource.getValue().length > 0 ? 'colored' : ''}}"
[ngbPopover]="popContent" title="Activity" placement="bottom" [popoverClass]="'nav-events'">
<i aria-hidden="true" class="fa fa-wave-square"></i>
</button>
<ng-template #popContent>
<ul class="list-group list-group-flush dark-menu">
<li class="list-group-item dark-menu-item" *ngFor="let event of progressEvents$ | async">
<div class="spinner-border text-primary small-spinner"
role="status" title="Started at {{event.timestamp | date: 'short'}}"
attr.aria-valuetext="{{prettyPrintProgress(event.progress)}}%" [attr.aria-valuenow]="prettyPrintProgress(event.progress)">
<span class="sr-only">Scan for {{event.libraryName}} in progress</span>
</div>
{{prettyPrintProgress(event.progress)}}%
{{prettyPrintEvent(event.eventType)}} {{event.libraryName}}
</li>
<li class="list-group-item dark-menu-item" *ngIf="progressEventsSource.getValue().length === 0">Not much going on here</li>
</ul>
</ng-template>
</ng-container>

View file

@ -0,0 +1,23 @@
@import "../../theme/colors";
.small-spinner {
width: 1rem;
height: 1rem;
}
.nav-events {
background-color: white;
}
.nav-events .popover-body {
padding: 0px;
}
.btn-icon {
color: white;
}
.colored {
background-color: $primary-color;
border-radius: 60px;
}

View file

@ -0,0 +1,90 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ProgressEvent } from '../_models/events/scan-library-progress-event';
import { User } from '../_models/user';
import { LibraryService } from '../_services/library.service';
import { EVENTS, Message, MessageHubService } from '../_services/message-hub.service';
interface ProcessedEvent {
eventType: string;
timestamp?: string;
progress: number;
libraryId: number;
libraryName: string;
}
type ProgressType = EVENTS.ScanLibraryProgress | EVENTS.RefreshMetadataProgress | EVENTS.BackupDatabaseProgress | EVENTS.CleanupProgress;
@Component({
selector: 'app-nav-events-toggle',
templateUrl: './nav-events-toggle.component.html',
styleUrls: ['./nav-events-toggle.component.scss']
})
export class NavEventsToggleComponent implements OnInit, OnDestroy {
@Input() user!: User;
private readonly onDestroy = new Subject<void>();
/**
* Events that come through and are merged (ie progress event gets merged into a progress event)
*/
progressEventsSource = new BehaviorSubject<ProcessedEvent[]>([]);
progressEvents$ = this.progressEventsSource.asObservable();
constructor(private messageHub: MessageHubService, private libraryService: LibraryService) { }
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
this.progressEventsSource.complete();
}
ngOnInit(): void {
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
if (event.event === EVENTS.ScanLibraryProgress || event.event === EVENTS.RefreshMetadataProgress || event.event === EVENTS.BackupDatabaseProgress || event.event === EVENTS.CleanupProgress) {
this.processProgressEvent(event, event.event);
}
});
}
processProgressEvent(event: Message<ProgressEvent>, eventType: string) {
const scanEvent = event.payload as ProgressEvent;
console.log(event.event, event.payload);
this.libraryService.getLibraryNames().subscribe(names => {
const data = this.progressEventsSource.getValue();
const index = data.findIndex(item => item.eventType === eventType && item.libraryId === event.payload.libraryId);
if (index >= 0) {
data.splice(index, 1);
}
if (scanEvent.progress !== 1) {
const libraryName = names[scanEvent.libraryId] || '';
const newEvent = {eventType: eventType, timestamp: scanEvent.eventTime, progress: scanEvent.progress, libraryId: scanEvent.libraryId, libraryName};
data.push(newEvent);
}
this.progressEventsSource.next(data);
});
}
prettyPrintProgress(progress: number) {
return Math.trunc(progress * 100);
}
prettyPrintEvent(eventType: string) {
switch(eventType) {
case (EVENTS.ScanLibraryProgress): return 'Scanning ';
case (EVENTS.RefreshMetadataProgress): return 'Refreshing ';
case (EVENTS.CleanupProgress): return 'Clearing Cache';
case (EVENTS.BackupDatabaseProgress): return 'Backing up Database';
default: return eventType;
}
}
}

View file

@ -62,6 +62,10 @@
</button>
</div>
<div class="nav-item" *ngIf="(accountService.currentUser$ | async) as user">
<app-nav-events-toggle [user]="user"></app-nav-events-toggle>
</div>
<div ngbDropdown class="nav-item dropdown" display="dynamic" placement="bottom-right" *ngIf="(accountService.currentUser$ | async) as user" dropdown>
<button class="btn btn-outline-secondary primary-text" ngbDropdownToggle>
{{user.username | sentenceCase}}

View file

@ -1,4 +1,4 @@
@import '~bootstrap/scss/mixins/_breakpoints.scss';
@import '~bootstrap/scss/mixins/_breakpoints.scss'; // TODO: Use @forwards for this?
$primary-color: white;
$bg-color: rgb(22, 27, 34);

View file

@ -11,6 +11,7 @@ export class NotConnectedComponent implements OnInit {
constructor(private memberService: MemberService, private router: Router) { }
ngOnInit(): void {
// BUG: TODO: This causes an infinite reload loop on the UI when the API on backend doesn't exist
// We make a call to backend on refresh so that if it's up, we can redirect to /home
this.memberService.adminExists().subscribe((exists) => {
const pageResume = localStorage.getItem('kavita--no-connection-url');

View file

@ -1,5 +1,5 @@
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout header="In Progress"
<app-card-detail-layout header="On Deck"
[isLoading]="isLoading"
[items]="series"
[filters]="filters"

View file

@ -13,11 +13,11 @@ import { ActionService } from '../_services/action.service';
import { SeriesService } from '../_services/series.service';
@Component({
selector: 'app-in-progress',
templateUrl: './in-progress.component.html',
styleUrls: ['./in-progress.component.scss']
selector: 'app-on-deck',
templateUrl: './on-deck.component.html',
styleUrls: ['./on-deck.component.scss']
})
export class InProgressComponent implements OnInit {
export class OnDeckComponent implements OnInit {
isLoading: boolean = true;
series: Series[] = [];
@ -31,7 +31,7 @@ export class InProgressComponent implements OnInit {
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 - In Progress');
this.titleService.setTitle('Kavita - On Deck');
if (this.pagination === undefined || this.pagination === null) {
this.pagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1};
}
@ -79,7 +79,7 @@ export class InProgressComponent implements OnInit {
this.pagination.currentPage = parseInt(page, 10);
}
this.isLoading = true;
this.seriesService.getInProgress(this.libraryId, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
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;

View file

@ -136,7 +136,12 @@ export class ReadingListDetailComponent implements OnInit {
return 'Volume ' + this.utilityService.cleanSpecialTitle(item.chapterNumber);
}
return this.utilityService.formatChapterName(this.libraryTypes[item.libraryId], true, true) + item.chapterNumber;
let chapterNum = item.chapterNumber;
if (!item.chapterNumber.match(/^\d+$/)) {
chapterNum = this.utilityService.cleanSpecialTitle(item.chapterNumber);
}
return this.utilityService.formatChapterName(this.libraryTypes[item.libraryId], true, true) + chapterNum;
}
orderUpdated(event: IndexUpdateEvent) {

View file

@ -10,13 +10,13 @@ import { SeriesAddedEvent } from '../_models/events/series-added-event';
import { Pagination } from '../_models/pagination';
import { Series } from '../_models/series';
import { FilterItem, mangaFormatFilters, SeriesFilter } from '../_models/series-filter';
import { Action, ActionFactoryService } from '../_services/action-factory.service';
import { Action } from '../_services/action-factory.service';
import { ActionService } from '../_services/action.service';
import { MessageHubService } from '../_services/message-hub.service';
import { SeriesService } from '../_services/series.service';
/**
* This component is used as a standard layout for any card detail. ie) series, in-progress, collections, etc.
* This component is used as a standard layout for any card detail. ie) series, on-deck, collections, etc.
*/
@Component({
selector: 'app-recently-added',

View file

@ -14,4 +14,5 @@
input {
background-color: #fff !important;
color: black !important;
}

View file

@ -16,6 +16,7 @@ import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
import { ReviewSeriesModalComponent } from '../_modals/review-series-modal/review-series-modal.component';
import { Chapter } from '../_models/chapter';
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
import { LibraryType } from '../_models/library';
import { MangaFormat } from '../_models/manga-format';
import { Series } from '../_models/series';
@ -26,7 +27,7 @@ import { ActionItem, ActionFactoryService, Action } from '../_services/action-fa
import { ActionService } from '../_services/action.service';
import { ImageService } from '../_services/image.service';
import { LibraryService } from '../_services/library.service';
import { MessageHubService } from '../_services/message-hub.service';
import { EVENTS, MessageHubService } from '../_services/message-hub.service';
import { ReaderService } from '../_services/reader.service';
import { SeriesService } from '../_services/series.service';
@ -180,6 +181,16 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.toastr.success('Scan series completed');
});
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(event => {
if (event.event === EVENTS.SeriesRemoved) {
const seriesRemovedEvent = event.payload as SeriesRemovedEvent;
if (seriesRemovedEvent.seriesId === this.series.id) {
this.toastr.info('This series no longer exists');
this.router.navigateByUrl('/libraries');
}
}
});
const seriesId = parseInt(routeId, 10);
this.libraryId = parseInt(libraryId, 10);
this.seriesImage = this.imageService.getSeriesCoverImage(seriesId);

View file

@ -158,5 +158,4 @@ export class UtilityService {
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
}

View file

@ -1,5 +1,3 @@
@import '../../../theme/colors';
$bg-color: #c9c9c9;
$bdr-color: #f2f2f2;

View file

@ -1,4 +1,4 @@
@import '../../assets/themes/dark.scss';
@import "../../theme/colors";
input {
width: 15px;

View file

@ -5,6 +5,8 @@ import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap
import { KEY_CODES } from '../shared/_services/utility.service';
import { 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.
@ -30,10 +32,16 @@ export class SelectionModel<T> {
/**
* Will toggle if the data item is selected or not. If data option is not tracked, will add it and set state to true.
* @param data Item to toggle
* @param selectedState Force the state
* @param compareFn An optional function to use for the lookup, else will use shallowEqual implementation
*/
toggle(data: T, selectedState?: boolean) {
//const dataItem = this._data.filter(d => d.value == data);
const dataItem = this._data.filter(d => this.shallowEqual(d.value, data));
toggle(data: T, selectedState?: boolean, compareFn?: SelectionCompareFn<T>) {
let lookupMethod = this.shallowEqual;
if (compareFn != undefined || compareFn != null) {
lookupMethod = compareFn;
}
const dataItem = this._data.filter(d => lookupMethod(d.value, data));
if (dataItem.length > 0) {
if (selectedState != undefined) {
dataItem[0].selected = selectedState;
@ -45,6 +53,7 @@ export class SelectionModel<T> {
}
}
/**
* Is the passed item selected
* @param data item to check against
@ -65,6 +74,15 @@ export class SelectionModel<T> {
return false;
}
/**
*
* @returns If some of the items are selected, but not all
*/
hasSomeSelected(): boolean {
const selectedCount = this._data.filter(d => d.selected).length;
return (selectedCount !== this._data.length && selectedCount !== 0)
}
/**
*
* @returns All Selected items

View file

@ -1,4 +1,4 @@
@import "../../theme/_colors.scss";
@use "../../theme/colors";
.login {
display: flex;
@ -36,7 +36,7 @@
}
.card {
background-color: $primary-color;
background-color: colors.$primary-color;
color: #fff;
cursor: pointer;
min-width: 300px;

View file

@ -1,17 +1,6 @@
// All dark style overrides should live here
$dark-bg-color: #343a40;
$dark-primary-color: rgba(74, 198, 148, 0.9);
$dark-text-color: #efefef;
$dark-hover-color: #4ac694;
$dark-form-background: rgba(1, 4, 9, 0.5);
$dark-form-background-no-opacity: rgb(1, 4, 9);
$dark-form-placeholder: #efefef;
$dark-link-color: rgb(88, 166, 255);
$dark-icon-color: white;
$dark-form-border: rgba(239, 239, 239, 0.125);
$dark-form-readonly: #434648;
$dark-item-accent-bg: #292d32;
@use "../../theme/colors";
.bg-dark {
color: $dark-text-color;
@ -43,6 +32,10 @@ $dark-item-accent-bg: #292d32;
box-shadow: inset 0px 0px 8px 1px $dark-form-background !important;
}
.text-muted {
color: #d7d7d7 !important;
}
.breadcrumb {
background-color: $dark-item-accent-bg;
@ -94,6 +87,17 @@ $dark-item-accent-bg: #292d32;
border-color: $dark-form-border;
}
.dark-menu {
background-color: $dark-form-background-no-opacity;
border-color: $dark-form-background;
}
.dark-menu-item {
color: $dark-text-color;
background-color: $dark-form-background-no-opacity;
border-color: $dark-form-background;
}
.dropdown .list-group-item:hover {
background-color: $dark-hover-color;
}
@ -166,7 +170,7 @@ $dark-item-accent-bg: #292d32;
.card {
background-color: $dark-bg-color;
color: $dark-text-color;
border-color: $dark-form-border; //#343a40
border-color: $dark-form-border;
}
.section-title {
@ -177,6 +181,14 @@ $dark-item-accent-bg: #292d32;
color: #efefef;
}
.nav-events, .nav-events .popover-body {
background-color: $dark-form-background-no-opacity;
}
.bs-popover-bottom > .arrow::after, .bs-popover-bottom > .arrow::before {
border-bottom-color: transparent;
}
}

View file

@ -1,6 +1,6 @@
// Import colors for overrides of bootstrap theme
@import './theme/_colors.scss';
@import './theme/_toastr.scss';
@import './theme/colors';
@import './theme/toastr';
// Bootstrap must be after _colors since we define the colors there
@import '~bootstrap/scss/bootstrap';
@ -32,7 +32,7 @@ label, select, .clickable {
@font-face {
font-family: "EBGarmond";
src: url(assets/fonts/EBGarmond/EBGaramond-VariableFont_wght.ttf) format("truetype");
src: url("assets/fonts/EBGarmond/EBGaramond-VariableFont_wght.ttf") format("truetype");
}
@font-face {

View file

@ -1,6 +1,19 @@
$primary-color: #4ac694; //(74,198,148)
$error-color: #ff4136; // #bb2929 good color for contrast rating
$dark-bg-color: #343a40;
$dark-primary-color: rgba(74, 198, 148, 0.9);
$dark-text-color: #efefef;
$dark-hover-color: #4ac694;
$dark-form-background: rgba(1, 4, 9, 0.5);
$dark-form-background-no-opacity: rgb(1, 4, 9);
$dark-form-placeholder: #efefef;
$dark-link-color: rgb(88, 166, 255);
$dark-icon-color: white;
$dark-form-border: rgba(239, 239, 239, 0.125);
$dark-form-readonly: #434648;
$dark-item-accent-bg: #292d32;
$theme-colors: (
"primary": $primary-color,
"danger": $error-color