Better Themes, Stats, and bugfixes (#1740)
* Fixed a bug where when clicking on a series rating for first time, the rating wasn't populating in the modal. * Fixed a bug on Scroll mode with immersive mode, the bottom bar could clip with the book body. * Cleanup some uses of var * Refactored text as json into a type so I don't have to copy/paste everywhere * Theme styles now override the defaults and theme owners no longer need to maintain all the variables themselves. Themes can now override the color of the header on mobile devices via --theme-color and Kavita will now update both theme color as well as color scheme. * Fixed a bug where last active on user stats wasn't for the particular user. * Added a more accurate word count calculation and the ability to see the word counts year over year. * Added a new table for long term statistics, like number of files over the years. No views are present for this data, I will add them later.
This commit is contained in:
parent
84b7978587
commit
5613d1a954
39 changed files with 2234 additions and 103 deletions
|
@ -13,6 +13,7 @@ import { UserUpdateEvent } from '../_models/events/user-update-event';
|
|||
import { UpdateEmailResponse } from '../_models/auth/update-email-response';
|
||||
import { AgeRating } from '../_models/metadata/age-rating';
|
||||
import { AgeRestriction } from '../_models/metadata/age-restriction';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
|
||||
export enum Role {
|
||||
Admin = 'Admin',
|
||||
|
@ -151,7 +152,7 @@ export class AccountService implements OnDestroy {
|
|||
}
|
||||
|
||||
migrateUser(model: {email: string, username: string, password: string, sendEmail: boolean}) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/migrate-email', model, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/migrate-email', model, TextResonse);
|
||||
}
|
||||
|
||||
confirmMigrationEmail(model: {email: string, token: string}) {
|
||||
|
@ -159,7 +160,7 @@ export class AccountService implements OnDestroy {
|
|||
}
|
||||
|
||||
resendConfirmationEmail(userId: number) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, TextResonse);
|
||||
}
|
||||
|
||||
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>, ageRestriction: AgeRestriction}) {
|
||||
|
@ -180,7 +181,7 @@ export class AccountService implements OnDestroy {
|
|||
* @returns
|
||||
*/
|
||||
getInviteUrl(userId: number, withBaseUrl: boolean = true) {
|
||||
return this.httpClient.get<string>(this.baseUrl + 'account/invite-url?userId=' + userId + '&withBaseUrl=' + withBaseUrl, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.get<string>(this.baseUrl + 'account/invite-url?userId=' + userId + '&withBaseUrl=' + withBaseUrl, TextResonse);
|
||||
}
|
||||
|
||||
getDecodedToken(token: string) {
|
||||
|
@ -188,15 +189,15 @@ export class AccountService implements OnDestroy {
|
|||
}
|
||||
|
||||
requestResetPasswordEmail(email: string) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/forgot-password?email=' + encodeURIComponent(email), {}, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/forgot-password?email=' + encodeURIComponent(email), {}, TextResonse);
|
||||
}
|
||||
|
||||
confirmResetPasswordEmail(model: {email: string, token: string, password: string}) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/confirm-password-reset', model, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/confirm-password-reset', model, TextResonse);
|
||||
}
|
||||
|
||||
resetPassword(username: string, password: string, oldPassword: string) {
|
||||
return this.httpClient.post(this.baseUrl + 'account/reset-password', {username, password, oldPassword}, {responseType: 'json' as 'text'});
|
||||
return this.httpClient.post(this.baseUrl + 'account/reset-password', {username, password, oldPassword}, TextResonse);
|
||||
}
|
||||
|
||||
update(model: {email: string, roles: Array<string>, libraries: Array<number>, userId: number, ageRestriction: AgeRestriction}) {
|
||||
|
@ -247,7 +248,7 @@ export class AccountService implements OnDestroy {
|
|||
}
|
||||
|
||||
resetApiKey() {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/reset-api-key', {}, {responseType: 'text' as 'json'}).pipe(map(key => {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/reset-api-key', {}, TextResonse).pipe(map(key => {
|
||||
const user = this.getUserFromLocalStorage();
|
||||
if (user) {
|
||||
user.apiKey = key;
|
||||
|
@ -264,7 +265,8 @@ export class AccountService implements OnDestroy {
|
|||
private refreshToken() {
|
||||
if (this.currentUser === null || this.currentUser === undefined) return of();
|
||||
|
||||
return this.httpClient.post<{token: string, refreshToken: string}>(this.baseUrl + 'account/refresh-token', {token: this.currentUser.token, refreshToken: this.currentUser.refreshToken}).pipe(map(user => {
|
||||
return this.httpClient.post<{token: string, refreshToken: string}>(this.baseUrl + 'account/refresh-token',
|
||||
{token: this.currentUser.token, refreshToken: this.currentUser.refreshToken}).pipe(map(user => {
|
||||
if (this.currentUser) {
|
||||
this.currentUser.token = user.token;
|
||||
this.currentUser.refreshToken = user.refreshToken;
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
|||
import { map } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { CollectionTag } from '../_models/collection-tag';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import { ImageService } from './image.service';
|
||||
|
||||
@Injectable({
|
||||
|
@ -26,15 +27,15 @@ export class CollectionTagService {
|
|||
}
|
||||
|
||||
updateTag(tag: CollectionTag) {
|
||||
return this.httpClient.post(this.baseUrl + 'collection/update', tag, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post(this.baseUrl + 'collection/update', tag, TextResonse);
|
||||
}
|
||||
|
||||
updateSeriesForTag(tag: CollectionTag, seriesIdsToRemove: Array<number>) {
|
||||
return this.httpClient.post(this.baseUrl + 'collection/update-series', {tag, seriesIdsToRemove}, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post(this.baseUrl + 'collection/update-series', {tag, seriesIdsToRemove}, TextResonse);
|
||||
}
|
||||
|
||||
addByMultiple(tagId: number, seriesIds: Array<number>, tagTitle: string = '') {
|
||||
return this.httpClient.post(this.baseUrl + 'collection/update-for-series', {collectionTagId: tagId, collectionTagTitle: tagTitle, seriesIds}, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post(this.baseUrl + 'collection/update-for-series', {collectionTagId: tagId, collectionTagTitle: tagTitle, seriesIds}, TextResonse);
|
||||
}
|
||||
|
||||
tagNameExists(name: string) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ReplaySubject, shareReplay, tap } from 'rxjs';
|
|||
import { environment } from 'src/environments/environment';
|
||||
import { Device } from '../_models/device/device';
|
||||
import { DevicePlatform } from '../_models/device/device-platform';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import { AccountService } from './account.service';
|
||||
|
||||
@Injectable({
|
||||
|
@ -32,11 +33,11 @@ export class DeviceService {
|
|||
}
|
||||
|
||||
createDevice(name: string, platform: DevicePlatform, emailAddress: string) {
|
||||
return this.httpClient.post(this.baseUrl + 'device/create', {name, platform, emailAddress}, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post(this.baseUrl + 'device/create', {name, platform, emailAddress}, TextResonse);
|
||||
}
|
||||
|
||||
updateDevice(id: number, name: string, platform: DevicePlatform, emailAddress: string) {
|
||||
return this.httpClient.post(this.baseUrl + 'device/update', {id, name, platform, emailAddress}, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post(this.baseUrl + 'device/update', {id, name, platform, emailAddress}, TextResonse);
|
||||
}
|
||||
|
||||
deleteDevice(id: number) {
|
||||
|
@ -50,7 +51,7 @@ export class DeviceService {
|
|||
}
|
||||
|
||||
sendTo(chapterIds: Array<number>, deviceId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'device/send-to', {deviceId, chapterIds}, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post(this.baseUrl + 'device/send-to', {deviceId, chapterIds}, TextResonse);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Language } from '../_models/metadata/language';
|
|||
import { PublicationStatusDto } from '../_models/metadata/publication-status-dto';
|
||||
import { Person } from '../_models/metadata/person';
|
||||
import { Tag } from '../_models/tag';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -28,7 +29,7 @@ export class MetadataService {
|
|||
if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) {
|
||||
return of(this.ageRatingTypes[ageRating]);
|
||||
}
|
||||
return this.httpClient.get<string>(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, {responseType: 'text' as 'json'}).pipe(map(ratingString => {
|
||||
return this.httpClient.get<string>(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, TextResonse).pipe(map(ratingString => {
|
||||
if (this.ageRatingTypes === undefined) {
|
||||
this.ageRatingTypes = {};
|
||||
}
|
||||
|
@ -97,6 +98,6 @@ export class MetadataService {
|
|||
}
|
||||
|
||||
getChapterSummary(chapterId: number) {
|
||||
return this.httpClient.get<string>(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.get<string>(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, TextResonse);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { UtilityService } from '../shared/_services/utility.service';
|
|||
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||
import { FileDimension } from '../manga-reader/_models/file-dimension';
|
||||
import screenfull from 'screenfull';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
|
||||
export const CHAPTER_ID_DOESNT_EXIST = -1;
|
||||
export const CHAPTER_ID_NOT_FETCHED = -2;
|
||||
|
@ -78,10 +79,10 @@ export class ReaderService {
|
|||
}
|
||||
|
||||
clearBookmarks(seriesId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'reader/remove-bookmarks', {seriesId}, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post(this.baseUrl + 'reader/remove-bookmarks', {seriesId}, TextResonse);
|
||||
}
|
||||
clearMultipleBookmarks(seriesIds: Array<number>) {
|
||||
return this.httpClient.post(this.baseUrl + 'reader/bulk-remove-bookmarks', {seriesIds}, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post(this.baseUrl + 'reader/bulk-remove-bookmarks', {seriesIds}, TextResonse);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,7 @@ import { environment } from 'src/environments/environment';
|
|||
import { UtilityService } from '../shared/_services/utility.service';
|
||||
import { PaginatedResult } from '../_models/pagination';
|
||||
import { ReadingList, ReadingListItem } from '../_models/reading-list';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import { ActionItem } from './action-factory.service';
|
||||
|
||||
@Injectable({
|
||||
|
@ -44,43 +45,43 @@ export class ReadingListService {
|
|||
}
|
||||
|
||||
update(model: {readingListId: number, title?: string, summary?: string, promoted: boolean}) {
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update', model, { responseType: 'text' as 'json' });
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update', model, TextResonse);
|
||||
}
|
||||
|
||||
updateByMultiple(readingListId: number, seriesId: number, volumeIds: Array<number>, chapterIds?: Array<number>) {
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-multiple', {readingListId, seriesId, volumeIds, chapterIds}, { responseType: 'text' as 'json' });
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-multiple', {readingListId, seriesId, volumeIds, chapterIds}, TextResonse);
|
||||
}
|
||||
|
||||
updateByMultipleSeries(readingListId: number, seriesIds: Array<number>) {
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-multiple-series', {readingListId, seriesIds}, { responseType: 'text' as 'json' });
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-multiple-series', {readingListId, seriesIds}, TextResonse);
|
||||
}
|
||||
|
||||
updateBySeries(readingListId: number, seriesId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-series', {readingListId, seriesId}, { responseType: 'text' as 'json' });
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-series', {readingListId, seriesId}, TextResonse);
|
||||
}
|
||||
|
||||
updateByVolume(readingListId: number, seriesId: number, volumeId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-volume', {readingListId, seriesId, volumeId}, { responseType: 'text' as 'json' });
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-volume', {readingListId, seriesId, volumeId}, TextResonse);
|
||||
}
|
||||
|
||||
updateByChapter(readingListId: number, seriesId: number, chapterId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-chapter', {readingListId, seriesId, chapterId}, { responseType: 'text' as 'json' });
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-by-chapter', {readingListId, seriesId, chapterId}, TextResonse);
|
||||
}
|
||||
|
||||
delete(readingListId: number) {
|
||||
return this.httpClient.delete(this.baseUrl + 'readinglist?readingListId=' + readingListId, { responseType: 'text' as 'json' });
|
||||
return this.httpClient.delete(this.baseUrl + 'readinglist?readingListId=' + readingListId, TextResonse);
|
||||
}
|
||||
|
||||
updatePosition(readingListId: number, readingListItemId: number, fromPosition: number, toPosition: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-position', {readingListId, readingListItemId, fromPosition, toPosition}, { responseType: 'text' as 'json' });
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/update-position', {readingListId, readingListItemId, fromPosition, toPosition}, TextResonse);
|
||||
}
|
||||
|
||||
deleteItem(readingListId: number, readingListItemId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/delete-item', {readingListId, readingListItemId}, { responseType: 'text' as 'json' });
|
||||
return this.httpClient.post(this.baseUrl + 'readinglist/delete-item', {readingListId, readingListItemId}, TextResonse);
|
||||
}
|
||||
|
||||
removeRead(readingListId: number) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'readinglist/remove-read?readingListId=' + readingListId, {}, { responseType: 'text' as 'json' });
|
||||
return this.httpClient.post<string>(this.baseUrl + 'readinglist/remove-read?readingListId=' + readingListId, {}, TextResonse);
|
||||
}
|
||||
|
||||
actionListFilter(action: ActionItem<ReadingList>, readingList: ReadingList, isAdmin: boolean) {
|
||||
|
|
|
@ -17,6 +17,7 @@ import { SeriesGroup } from '../_models/series-group';
|
|||
import { SeriesMetadata } from '../_models/metadata/series-metadata';
|
||||
import { Volume } from '../_models/volume';
|
||||
import { ImageService } from './image.service';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -131,7 +132,7 @@ export class SeriesService {
|
|||
}
|
||||
|
||||
isWantToRead(seriesId: number) {
|
||||
return this.httpClient.get<string>(this.baseUrl + 'want-to-read?seriesId=' + seriesId, {responseType: 'text' as 'json'})
|
||||
return this.httpClient.get<string>(this.baseUrl + 'want-to-read?seriesId=' + seriesId, TextResonse)
|
||||
.pipe(map(val => {
|
||||
return val === 'true';
|
||||
}));
|
||||
|
@ -174,7 +175,7 @@ export class SeriesService {
|
|||
seriesMetadata,
|
||||
collectionTags,
|
||||
};
|
||||
return this.httpClient.post(this.baseUrl + 'series/metadata', data, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post(this.baseUrl + 'series/metadata', data, TextResonse);
|
||||
}
|
||||
|
||||
getSeriesForTag(collectionTagId: number, pageNum?: number, itemsPerPage?: number) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ServerStatistics } from '../statistics/_models/server-statistics';
|
|||
import { StatCount } from '../statistics/_models/stat-count';
|
||||
import { PublicationStatus } from '../_models/metadata/publication-status';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
|
||||
export enum DayOfWeek
|
||||
{
|
||||
|
@ -58,7 +59,21 @@ export class StatisticsService {
|
|||
getTopYears() {
|
||||
return this.httpClient.get<StatCount<number>[]>(this.baseUrl + 'stats/server/top/years').pipe(
|
||||
map(spreads => spreads.map(spread => {
|
||||
return {name: spread.value + '', value: spread.count};
|
||||
return {name: spread.value + '', value: spread.count};
|
||||
})));
|
||||
}
|
||||
|
||||
getPagesPerYear(userId = 0) {
|
||||
return this.httpClient.get<StatCount<number>[]>(this.baseUrl + 'stats/pages-per-year?userId=' + userId).pipe(
|
||||
map(spreads => spreads.map(spread => {
|
||||
return {name: spread.value + '', value: spread.count};
|
||||
})));
|
||||
}
|
||||
|
||||
getWordsPerYear(userId = 0) {
|
||||
return this.httpClient.get<StatCount<number>[]>(this.baseUrl + 'stats/words-per-year?userId=' + userId).pipe(
|
||||
map(spreads => spreads.map(spread => {
|
||||
return {name: spread.value + '', value: spread.count};
|
||||
})));
|
||||
}
|
||||
|
||||
|
@ -85,7 +100,7 @@ export class StatisticsService {
|
|||
}
|
||||
|
||||
getTotalSize() {
|
||||
return this.httpClient.get<number>(this.baseUrl + 'stats/server/file-size', { responseType: 'text' as 'json'});
|
||||
return this.httpClient.get<number>(this.baseUrl + 'stats/server/file-size', TextResonse);
|
||||
}
|
||||
|
||||
getFileBreakdown() {
|
||||
|
|
|
@ -3,12 +3,12 @@ import { HttpClient } from '@angular/common/http';
|
|||
import { Inject, Injectable, OnDestroy, Renderer2, RendererFactory2, SecurityContext } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { map, ReplaySubject, Subject, takeUntil, take, distinctUntilChanged, Observable } from 'rxjs';
|
||||
import { map, ReplaySubject, Subject, takeUntil, take } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { ConfirmService } from '../shared/confirm.service';
|
||||
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
||||
import { SiteTheme, ThemeProvider } from '../_models/preferences/site-theme';
|
||||
import { AccountService } from './account.service';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import { EVENTS, MessageHubService } from './message-hub.service';
|
||||
|
||||
|
||||
|
@ -65,6 +65,14 @@ export class ThemeService implements OnDestroy {
|
|||
return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* --theme-color from theme. Updates the meta tag
|
||||
* @returns
|
||||
*/
|
||||
getThemeColor() {
|
||||
return getComputedStyle(this.document.body).getPropertyValue('--theme-color').trim();
|
||||
}
|
||||
|
||||
getCssVariable(variable: string) {
|
||||
return getComputedStyle(this.document.body).getPropertyValue(variable).trim();
|
||||
}
|
||||
|
@ -137,11 +145,23 @@ export class ThemeService implements OnDestroy {
|
|||
this.setTheme('dark');
|
||||
return;
|
||||
}
|
||||
const styleElem = document.createElement('style');
|
||||
const styleElem = this.document.createElement('style');
|
||||
styleElem.id = 'theme-' + theme.name;
|
||||
styleElem.appendChild(this.document.createTextNode(content));
|
||||
|
||||
this.renderer.appendChild(this.document.head, styleElem);
|
||||
|
||||
// Check if the theme has --theme-color and apply it to meta tag
|
||||
const themeColor = this.getThemeColor();
|
||||
if (themeColor) {
|
||||
this.document.querySelector('meta[name="theme-color"]')?.setAttribute('content', themeColor);
|
||||
}
|
||||
|
||||
const colorScheme = this.getColorScheme();
|
||||
if (themeColor) {
|
||||
this.document.querySelector('body')?.setAttribute('theme', colorScheme);
|
||||
}
|
||||
|
||||
this.currentThemeSource.next(theme);
|
||||
});
|
||||
} else {
|
||||
|
@ -161,8 +181,7 @@ export class ThemeService implements OnDestroy {
|
|||
}
|
||||
|
||||
private fetchThemeContent(themeId: number) {
|
||||
// TODO: Refactor {responseType: 'text' as 'json'} into a type so i don't have to retype it
|
||||
return this.httpClient.get<string>(this.baseUrl + 'theme/download-content?themeId=' + themeId, {responseType: 'text' as 'json'}).pipe(map(encodedCss => {
|
||||
return this.httpClient.get<string>(this.baseUrl + 'theme/download-content?themeId=' + themeId, TextResonse).pipe(map(encodedCss => {
|
||||
return this.domSantizer.sanitize(SecurityContext.STYLE, encodedCss);
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -13,7 +14,7 @@ export class UploadService {
|
|||
|
||||
|
||||
uploadByUrl(url: string) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'upload/upload-by-url', {url}, {responseType: 'text' as 'json'});
|
||||
return this.httpClient.post<string>(this.baseUrl + 'upload/upload-by-url', {url}, TextResonse);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
4
UI/Web/src/app/_types/text-response.ts
Normal file
4
UI/Web/src/app/_types/text-response.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Use when httpClient is expected to return just a string/variable and not json
|
||||
*/
|
||||
export const TextResonse = {responseType: 'text' as 'json'};
|
|
@ -1,6 +1,7 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import { ServerSettings } from './_models/server-settings';
|
||||
|
||||
/**
|
||||
|
@ -53,6 +54,6 @@ export class SettingsService {
|
|||
}
|
||||
|
||||
getOpdsEnabled() {
|
||||
return this.http.get<boolean>(this.baseUrl + 'settings/opds-enabled', {responseType: 'text' as 'json'});
|
||||
return this.http.get<boolean>(this.baseUrl + 'settings/opds-enabled', TextResonse);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,7 +91,8 @@
|
|||
[innerHtml]="page" *ngIf="page !== undefined" (click)="toggleMenu($event)" (mousedown)="mouseDown($event)"></div>
|
||||
|
||||
|
||||
<div *ngIf="page !== undefined && (scrollbarNeeded || layoutMode !== BookPageLayoutMode.Default)" (click)="$event.stopPropagation();" [ngClass]="{'bottom-bar': layoutMode !== BookPageLayoutMode.Default}">
|
||||
<div *ngIf="page !== undefined && (scrollbarNeeded || layoutMode !== BookPageLayoutMode.Default)" (click)="$event.stopPropagation();"
|
||||
[ngClass]="{'bottom-bar': layoutMode !== BookPageLayoutMode.Default}">
|
||||
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -47,7 +47,7 @@ $primary-color: #0062cc;
|
|||
$action-bar-height: 38px;
|
||||
|
||||
|
||||
// Drawer
|
||||
// Drawer
|
||||
.control-container {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
@ -86,10 +86,6 @@ $action-bar-height: 38px;
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
::ng-deep .bg-warning {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
|
||||
.action-bar {
|
||||
background-color: var(--br-actionbar-bg-color);
|
||||
|
@ -196,7 +192,8 @@ $action-bar-height: 38px;
|
|||
}
|
||||
|
||||
&.immersive {
|
||||
height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
|
||||
// Note: I removed this for bug: https://github.com/Kareadita/Kavita/issues/1726
|
||||
//height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
|
||||
}
|
||||
|
||||
a, :link {
|
||||
|
|
|
@ -262,9 +262,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
@ViewChild('reader', {static: true}) reader!: ElementRef;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
get BookPageLayoutMode() {
|
||||
return BookPageLayoutMode;
|
||||
}
|
||||
|
@ -722,16 +719,17 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
* from 'kavita-part', which will cause the reader to scroll to the marker.
|
||||
*/
|
||||
addLinkClickHandlers() {
|
||||
var links = this.readingSectionElemRef.nativeElement.querySelectorAll('a');
|
||||
const links = this.readingSectionElemRef.nativeElement.querySelectorAll('a');
|
||||
links.forEach((link: any) => {
|
||||
link.addEventListener('click', (e: any) => {
|
||||
console.log('Link clicked: ', e);
|
||||
if (!e.target.attributes.hasOwnProperty('kavita-page')) { return; }
|
||||
var page = parseInt(e.target.attributes['kavita-page'].value, 10);
|
||||
const page = parseInt(e.target.attributes['kavita-page'].value, 10);
|
||||
if (this.adhocPageHistory.peek()?.page !== this.pageNum) {
|
||||
this.adhocPageHistory.push({page: this.pageNum, scrollPart: this.lastSeenScrollPartPath});
|
||||
}
|
||||
|
||||
var partValue = e.target.attributes.hasOwnProperty('kavita-part') ? e.target.attributes['kavita-part'].value : undefined;
|
||||
const partValue = e.target.attributes.hasOwnProperty('kavita-part') ? e.target.attributes['kavita-part'].value : undefined;
|
||||
if (partValue && page === this.pageNum) {
|
||||
this.scrollTo(e.target.attributes['kavita-part'].value);
|
||||
return;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TextResonse } from 'src/app/_types/text-response';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { BookChapterItem } from '../_models/book-chapter-item';
|
||||
import { BookInfo } from '../_models/book-info';
|
||||
|
@ -41,7 +42,7 @@ export class BookService {
|
|||
}
|
||||
|
||||
getBookPage(chapterId: number, page: number) {
|
||||
return this.http.get<string>(this.baseUrl + 'book/' + chapterId + '/book-page?page=' + page, {responseType: 'text' as 'json'});
|
||||
return this.http.get<string>(this.baseUrl + 'book/' + chapterId + '/book-page?page=' + page, TextResonse);
|
||||
}
|
||||
|
||||
getBookInfo(chapterId: number) {
|
||||
|
|
|
@ -252,6 +252,13 @@ export class DoubleNoCoverRendererComponent implements OnInit {
|
|||
this.debugLog('Moving forward 2 pages');
|
||||
return 2;
|
||||
case PAGING_DIRECTION.BACKWARDS:
|
||||
|
||||
if (this.mangaReaderService.isCoverImage(this.pageNum - 1)) {
|
||||
// TODO: If we are moving back and prev page is cover and we are not showing on right side, then move back twice as if we did once, we would show pageNum twice
|
||||
this.debugLog('Moving back 1 page as on cover image');
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (this.mangaReaderService.isCoverImage(this.pageNum)) {
|
||||
this.debugLog('Moving back 1 page as on cover image');
|
||||
return 2;
|
||||
|
|
|
@ -670,7 +670,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||
return;
|
||||
}
|
||||
|
||||
this.seriesService.updateRating(this.series?.id, this.series?.userRating, this.series?.userReview).subscribe(() => {
|
||||
this.seriesService.updateRating(this.series?.id, rating, this.series?.userReview).subscribe(() => {
|
||||
this.series.userRating = rating;
|
||||
this.createHTML();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div class="list-group">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center" *ngFor="let item of items | filter: filterList; let i = index">
|
||||
{{item}}
|
||||
<button class="btn btn-primary" [disabled]="clicked === undefined" (click)="handleClick(item)">
|
||||
<button class="btn btn-primary" *ngIf="clicked !== undefined" (click)="handleClick(item)">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Open a filtered search for {{item}}</span>
|
||||
</button>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="row g-0 mt-4 mb-3 d-flex justify-content-around">
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Pages Read" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Total Pages Read">
|
||||
<app-icon-and-title label="Total Pages Read" [clickable]="true" fontClasses="fa-regular fa-file-lines" title="Total Pages Read" (click)="openPageByYearList();$event.stopPropagation();">
|
||||
{{totalPagesRead | compactNumber}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
@ -10,7 +10,7 @@
|
|||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Words Read" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Total Words Read">
|
||||
<app-icon-and-title label="Total Words Read" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Total Words Read" (click)="openWordByYearList();$event.stopPropagation();">
|
||||
{{totalWordsRead | compactNumber}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { CompactNumberPipe } from 'src/app/pipe/compact-number.pipe';
|
||||
import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service';
|
||||
import { StatisticsService } from 'src/app/_services/statistics.service';
|
||||
import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-stats-info-cards',
|
||||
|
@ -15,9 +20,27 @@ export class UserStatsInfoCardsComponent implements OnInit {
|
|||
@Input() lastActive: string = '';
|
||||
@Input() avgHoursPerWeekSpentReading: number = 0;
|
||||
|
||||
constructor() { }
|
||||
constructor(private statsService: StatisticsService, private modalService: NgbModal) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
openPageByYearList() {
|
||||
const numberPipe = new CompactNumberPipe();
|
||||
this.statsService.getPagesPerYear().subscribe(yearCounts => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = yearCounts.map(t => `${t.name}: ${numberPipe.transform(t.value)} pages`);
|
||||
ref.componentInstance.title = 'Pages Read By Year';
|
||||
});
|
||||
}
|
||||
|
||||
openWordByYearList() {
|
||||
const numberPipe = new CompactNumberPipe();
|
||||
this.statsService.getWordsPerYear().subscribe(yearCounts => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = yearCounts.map(t => `${t.name}: ${numberPipe.transform(t.value)} pages`);
|
||||
ref.componentInstance.title = 'Words Read By Year';
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
</head>
|
||||
<body class="mat-typography" theme="dark">
|
||||
<body class="mat-typography default" theme="dark">
|
||||
<app-root></app-root>
|
||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||
</body>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
|
||||
// Import themes which define the css variables we use to customize the app
|
||||
@import './theme/themes/light';
|
||||
@import './theme/themes/dark';
|
||||
|
||||
// Import colors for overrides of bootstrap theme
|
||||
|
|
|
@ -21,6 +21,6 @@ $grid-breakpoints-xl: 1200px;
|
|||
$grid-breakpoints: (xs: $grid-breakpoints-xs, sm: $grid-breakpoints-sm, md: $grid-breakpoints-md, lg: $grid-breakpoints-lg, xl: $grid-breakpoints-xl);
|
||||
|
||||
// Override any bootstrap styles we don't want
|
||||
:root {
|
||||
--hr-color: transparent;
|
||||
}
|
||||
// :root {
|
||||
// --hr-color: transparent;
|
||||
// }
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
:root, :root .bg-dark {
|
||||
//
|
||||
:root, :root .default {
|
||||
--theme-color: #000000;
|
||||
--color-scheme: dark;
|
||||
--primary-color: #4ac694;
|
||||
--primary-color-dark-shade: #3B9E76;
|
||||
|
@ -240,4 +242,7 @@
|
|||
|
||||
/* List Card Item */
|
||||
--card-list-item-bg-color: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
|
||||
|
||||
/* Bootstrap overrides */
|
||||
--hr-color: transparent;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue