Misc Bugfixes (#2216)
* Folder watching will now appropriately ignore changes that occur in blacklisted folders. * Fixed up recently updated from dashboard not opening a pre-sorted page. There were issues with how encoding and decoding was done plus missing code. * Fixed up all streams from Dashboard opening to correctly filtered pages. * All search linking now works. * Rating tooltip and stars are bigger on mobile. * A bit of cleanup * Added day breakdown to user stats page. * Removed Token checks before we write events to the history table for scrobbling. Refactored so series holds will prevent writing events for reviews, ratings, etc. * Fixed a potential bug where series name could be taken from a chapter that isn't the first ordered (very unlikely) for epubs. Fixed a bug where Volume 1.5 could be selected for series-level metadata over Volume 1. * Optimized the license check code so that users without any license entered would still take advantage of the cache layer. * Sped up an API that checks if the library allows scrobbling * Cleaned up the mobile CSS a bit for filters.
This commit is contained in:
parent
ef3e76e3e5
commit
c84a3294e9
30 changed files with 324 additions and 246 deletions
|
@ -112,7 +112,7 @@ export class StatisticsService {
|
|||
return this.httpClient.get<Array<any>>(this.baseUrl + 'stats/reading-count-by-day?userId=' + userId + '&days=' + days);
|
||||
}
|
||||
|
||||
getDayBreakdown() {
|
||||
return this.httpClient.get<Array<StatCount<DayOfWeek>>>(this.baseUrl + 'stats/day-breakdown');
|
||||
getDayBreakdown( userId = 0) {
|
||||
return this.httpClient.get<Array<StatCount<DayOfWeek>>>(this.baseUrl + 'stats/day-breakdown?userId=' + userId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ import { BulkOperationsComponent } from '../../../cards/bulk-operations/bulk-ope
|
|||
import { NgIf, DecimalPipe } from '@angular/common';
|
||||
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {FilterField} from "../../../_models/metadata/v2/filter-field";
|
||||
import {SeriesFilterV2} from "../../../_models/metadata/v2/series-filter-v2";
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChild,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
|
@ -13,7 +12,6 @@ import {
|
|||
Inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
TemplateRef,
|
||||
|
@ -70,12 +68,15 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
|||
* Any actions to exist on the header for the parent collection (library, collection)
|
||||
*/
|
||||
@Input() actions: ActionItem<any>[] = [];
|
||||
@Input() trackByIdentity!: TrackByFunction<any>; //(index: number, item: any) => string
|
||||
/**
|
||||
* A trackBy to help with rendering. This is required as without it there are issues when scrolling
|
||||
*/
|
||||
@Input({required: true}) trackByIdentity!: TrackByFunction<any>;
|
||||
@Input() filterSettings!: FilterSettings;
|
||||
@Input() refresh!: EventEmitter<void>;
|
||||
|
||||
|
||||
@Input() jumpBarKeys: Array<JumpKey> = []; // This is aprox 784 pixels tall, original keys
|
||||
@Input() jumpBarKeys: Array<JumpKey> = []; // This is approx 784 pixels tall, original keys
|
||||
jumpBarKeysToRender: Array<JumpKey> = []; // What is rendered on screen
|
||||
|
||||
@Output() itemClicked: EventEmitter<any> = new EventEmitter();
|
||||
|
@ -115,7 +116,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
|||
|
||||
ngOnInit(): void {
|
||||
if (this.trackByIdentity === undefined) {
|
||||
this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`;
|
||||
this.trackByIdentity = (_: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`;
|
||||
}
|
||||
|
||||
if (this.filterSettings === undefined) {
|
||||
|
|
|
@ -25,7 +25,6 @@ import {CompactNumberPipe} from "../../pipe/compact-number.pipe";
|
|||
import {AgeRatingPipe} from "../../pipe/age-rating.pipe";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component";
|
||||
import {FilterQueryParam} from "../../shared/_services/filter-utilities.service";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
|
||||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
|
@ -132,6 +131,4 @@ export class EntityInfoCardsComponent implements OnInit {
|
|||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
protected readonly FilterQueryParam = FilterQueryParam;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import {Title} from '@angular/platform-browser';
|
|||
import {Router, RouterLink} from '@angular/router';
|
||||
import {Observable, of, ReplaySubject} from 'rxjs';
|
||||
import {debounceTime, map, shareReplay, take, tap} from 'rxjs/operators';
|
||||
import {FilterQueryParam, FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
|
||||
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
|
||||
import {SeriesAddedEvent} from 'src/app/_models/events/series-added-event';
|
||||
import {SeriesRemovedEvent} from 'src/app/_models/events/series-removed-event';
|
||||
import {Library} from 'src/app/_models/library';
|
||||
|
@ -169,23 +169,36 @@ export class DashboardComponent implements OnInit {
|
|||
handleSectionClick(sectionTitle: string) {
|
||||
if (sectionTitle.toLowerCase() === 'recently updated series') {
|
||||
const params: any = {};
|
||||
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
|
||||
params[FilterQueryParam.Page] = 1;
|
||||
params['page'] = 1;
|
||||
params['title'] = 'Recently Updated';
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
const filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
if (filter.sortOptions) {
|
||||
filter.sortOptions.sortField = SortField.LastChapterAdded;
|
||||
filter.sortOptions.isAscending = false;
|
||||
}
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params)
|
||||
} else if (sectionTitle.toLowerCase() === 'on deck') {
|
||||
const params: any = {};
|
||||
params[FilterQueryParam.ReadStatus] = 'true,false,false';
|
||||
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
|
||||
params[FilterQueryParam.Page] = 1;
|
||||
params['page'] = 1;
|
||||
params['title'] = 'On Deck';
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
|
||||
const filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
filter.statements.push({field: FilterField.ReadProgress, comparison: FilterComparison.GreaterThan, value: '0'});
|
||||
if (filter.sortOptions) {
|
||||
filter.sortOptions.sortField = SortField.LastChapterAdded;
|
||||
filter.sortOptions.isAscending = false;
|
||||
}
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params)
|
||||
}else if (sectionTitle.toLowerCase() === 'newly added series') {
|
||||
const params: any = {};
|
||||
params[FilterQueryParam.SortBy] = SortField.Created + ',false'; // sort by created, desc
|
||||
params[FilterQueryParam.Page] = 1;
|
||||
params['page'] = 1;
|
||||
params['title'] = 'Newly Added';
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
const filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
if (filter.sortOptions) {
|
||||
filter.sortOptions.sortField = SortField.Created;
|
||||
filter.sortOptions.isAscending = false;
|
||||
}
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,13 +25,13 @@
|
|||
</div>
|
||||
<form [formGroup]="sortGroup" class="container-fluid">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-2 col-sm-2">
|
||||
<div class="form-group pe-1">
|
||||
<label for="limit-to" class="form-label">{{t('limit-label')}}</label>
|
||||
<input id="limit-to" type="number" inputmode="numeric" class="form-control" formControlName="limitTo">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-3 col-sm-10">
|
||||
<label for="sort-options" class="form-label">{{t('sort-by-label')}}</label>
|
||||
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0;" [disabled]="filterSettings.sortDisabled">
|
||||
<i class="fa fa-arrow-up" [title]="t('ascending-alt')" *ngIf="isAscendingSort; else descSort"></i>
|
||||
|
@ -43,15 +43,23 @@
|
|||
<option *ngFor="let field of allSortFields" [value]="field">{{field | sortField}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- TODO: I might want to put a Clear button which blanks out the whole filter -->
|
||||
<div class="col-md-2 me-3 mt-4">
|
||||
<button class="btn btn-secondary col-12" (click)="clear()">{{t('reset')}}</button>
|
||||
</div>
|
||||
<div class="col-md-2 me-3 mt-4">
|
||||
<button class="btn btn-primary col-12" (click)="apply()">{{t('apply')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="utilityService.getActiveBreakpoint() > Breakpoint.Tablet" [ngTemplateOutlet]="buttons"></ng-container>
|
||||
</div>
|
||||
<div class="row mb-3" *ngIf="utilityService.getActiveBreakpoint() <= Breakpoint.Tablet">
|
||||
<ng-container [ngTemplateOutlet]="buttons"></ng-container>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template #buttons>
|
||||
<!-- TODO: I might want to put a Clear button which blanks out the whole filter -->
|
||||
<div class="col-md-2 col-sm-6 mt-4">
|
||||
<button class="btn btn-secondary col-12" (click)="clear()">{{t('reset')}}</button>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 mt-4">
|
||||
<button class="btn btn-primary col-12" (click)="apply()">{{t('apply')}}</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||
|
||||
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
|
||||
|
||||
/**
|
||||
|
@ -82,10 +83,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
|
||||
|
||||
constructor(private utilityService: UtilityService,
|
||||
public toggleService: ToggleService,
|
||||
private filterUtilityService: FilterUtilitiesService) {
|
||||
}
|
||||
constructor(public toggleService: ToggleService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.filterSettings === undefined) {
|
||||
|
@ -203,4 +201,5 @@ export class MetadataFilterComponent implements OnInit {
|
|||
this.toggleService.set(!this.filteringCollapsed);
|
||||
}
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
</ng-template>
|
||||
|
||||
<ng-template #tagTemplate let-item>
|
||||
<div style="display: flex;padding: 5px;" (click)="goTo('tags', item.id)">
|
||||
<div style="display: flex;padding: 5px;" (click)="goToOther(FilterField.Tags, item.id)">
|
||||
<div class="ms-1">
|
||||
<span>{{item.title}}</span>
|
||||
</div>
|
||||
|
@ -97,7 +97,7 @@
|
|||
</ng-template>
|
||||
|
||||
<ng-template #genreTemplate let-item>
|
||||
<div style="display: flex;padding: 5px;" class="clickable" (click)="goTo('genres', item.id)">
|
||||
<div style="display: flex;padding: 5px;" class="clickable" (click)="goToOther(FilterField.Genres, item.id)">
|
||||
<div class="ms-1">
|
||||
<div [innerHTML]="item.title"></div>
|
||||
</div>
|
||||
|
|
|
@ -1,40 +1,44 @@
|
|||
import { DOCUMENT, NgIf, NgOptimizedImage, AsyncPipe } from '@angular/common';
|
||||
import {AsyncPipe, DOCUMENT, NgIf, NgOptimizedImage} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, DestroyRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
inject,
|
||||
Inject,
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { fromEvent } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, filter, tap } from 'rxjs/operators';
|
||||
import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { CollectionTag } from 'src/app/_models/collection-tag';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
import { PersonRole } from 'src/app/_models/metadata/person';
|
||||
import { ReadingList } from 'src/app/_models/reading-list';
|
||||
import { SearchResult } from 'src/app/_models/search/search-result';
|
||||
import { SearchResultGroup } from 'src/app/_models/search/search-result-group';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { NavService } from 'src/app/_services/nav.service';
|
||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||
import { SearchService } from 'src/app/_services/search.service';
|
||||
import {NavigationEnd, Router, RouterLink, RouterLinkActive} from '@angular/router';
|
||||
import {fromEvent} from 'rxjs';
|
||||
import {debounceTime, distinctUntilChanged, filter, tap} from 'rxjs/operators';
|
||||
import {Chapter} from 'src/app/_models/chapter';
|
||||
import {CollectionTag} from 'src/app/_models/collection-tag';
|
||||
import {Library} from 'src/app/_models/library';
|
||||
import {MangaFile} from 'src/app/_models/manga-file';
|
||||
import {PersonRole} from 'src/app/_models/metadata/person';
|
||||
import {ReadingList} from 'src/app/_models/reading-list';
|
||||
import {SearchResult} from 'src/app/_models/search/search-result';
|
||||
import {SearchResultGroup} from 'src/app/_models/search/search-result-group';
|
||||
import {AccountService} from 'src/app/_services/account.service';
|
||||
import {ImageService} from 'src/app/_services/image.service';
|
||||
import {NavService} from 'src/app/_services/nav.service';
|
||||
import {ScrollService} from 'src/app/_services/scroll.service';
|
||||
import {SearchService} from 'src/app/_services/search.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { SentenceCasePipe } from '../../../pipe/sentence-case.pipe';
|
||||
import { PersonRolePipe } from '../../../pipe/person-role.pipe';
|
||||
import { NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EventsWidgetComponent } from '../events-widget/events-widget.component';
|
||||
import { SeriesFormatComponent } from '../../../shared/series-format/series-format.component';
|
||||
import { ImageComponent } from '../../../shared/image/image.component';
|
||||
import { GroupedTypeaheadComponent } from '../grouped-typeahead/grouped-typeahead.component';
|
||||
import {SentenceCasePipe} from '../../../pipe/sentence-case.pipe';
|
||||
import {PersonRolePipe} from '../../../pipe/person-role.pipe';
|
||||
import {NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {EventsWidgetComponent} from '../events-widget/events-widget.component';
|
||||
import {SeriesFormatComponent} from '../../../shared/series-format/series-format.component';
|
||||
import {ImageComponent} from '../../../shared/image/image.component';
|
||||
import {GroupedTypeaheadComponent} from '../grouped-typeahead/grouped-typeahead.component';
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
|
||||
import {FilterStatement} from "../../../_models/metadata/v2/filter-statement";
|
||||
import {FilterField} from "../../../_models/metadata/v2/filter-field";
|
||||
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav-header',
|
||||
|
@ -58,10 +62,12 @@ export class NavHeaderComponent implements OnInit {
|
|||
backToTopNeeded = false;
|
||||
searchFocused: boolean = false;
|
||||
scrollElem: HTMLElement;
|
||||
protected readonly FilterField = FilterField;
|
||||
|
||||
constructor(public accountService: AccountService, private router: Router, public navService: NavService,
|
||||
public imageService: ImageService, @Inject(DOCUMENT) private document: Document,
|
||||
private scrollService: ScrollService, private searchService: SearchService, private readonly cdRef: ChangeDetectorRef) {
|
||||
private scrollService: ScrollService, private searchService: SearchService, private readonly cdRef: ChangeDetectorRef,
|
||||
private filterUtilityService: FilterUtilitiesService) {
|
||||
this.scrollElem = this.document.body;
|
||||
}
|
||||
|
||||
|
@ -69,12 +75,11 @@ export class NavHeaderComponent implements OnInit {
|
|||
this.scrollService.scrollContainer$.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef), tap((scrollContainer) => {
|
||||
if (scrollContainer === 'body' || scrollContainer === undefined) {
|
||||
this.scrollElem = this.document.body;
|
||||
fromEvent(this.document.body, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded(this.document.body));
|
||||
} else {
|
||||
const elem = scrollContainer as ElementRef<HTMLDivElement>;
|
||||
this.scrollElem = elem.nativeElement;
|
||||
fromEvent(elem.nativeElement, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded(elem.nativeElement));
|
||||
}
|
||||
fromEvent(this.scrollElem, 'scroll').pipe(debounceTime(20)).subscribe(() => this.checkBackToTopNeeded(this.scrollElem));
|
||||
})).subscribe();
|
||||
|
||||
// Sometimes the top event emitter can be slow, so let's also check when a navigation occurs and recalculate
|
||||
|
@ -125,49 +130,54 @@ export class NavHeaderComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
goTo(queryParamName: string, filter: any) {
|
||||
goTo(statement: FilterStatement) {
|
||||
let params: any = {};
|
||||
params[queryParamName] = filter;
|
||||
params[FilterQueryParam.Page] = 1;
|
||||
const filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
filter.statements = [statement];
|
||||
params['page'] = 1;
|
||||
this.clearSearch();
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params);
|
||||
}
|
||||
|
||||
goToOther(field: FilterField, value: string) {
|
||||
this.goTo({field, comparison: FilterComparison.Equal, value});
|
||||
}
|
||||
|
||||
goToPerson(role: PersonRole, filter: any) {
|
||||
this.clearSearch();
|
||||
switch(role) {
|
||||
case PersonRole.Writer:
|
||||
this.goTo(FilterQueryParam.Writers, filter);
|
||||
this.goTo({field: FilterField.Writers, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Artist:
|
||||
this.goTo(FilterQueryParam.Artists, filter);
|
||||
this.goTo({field: FilterField.CoverArtist, comparison: FilterComparison.Equal, value: filter}); // TODO: What is this supposed to be?
|
||||
break;
|
||||
case PersonRole.Character:
|
||||
this.goTo(FilterQueryParam.Character, filter);
|
||||
this.goTo({field: FilterField.Characters, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Colorist:
|
||||
this.goTo(FilterQueryParam.Colorist, filter);
|
||||
this.goTo({field: FilterField.Colorist, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Editor:
|
||||
this.goTo(FilterQueryParam.Editor, filter);
|
||||
this.goTo({field: FilterField.Editor, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Inker:
|
||||
this.goTo(FilterQueryParam.Inker, filter);
|
||||
this.goTo({field: FilterField.Inker, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.CoverArtist:
|
||||
this.goTo(FilterQueryParam.CoverArtists, filter);
|
||||
this.goTo({field: FilterField.CoverArtist, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Letterer:
|
||||
this.goTo(FilterQueryParam.Letterer, filter);
|
||||
this.goTo({field: FilterField.Letterer, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Penciller:
|
||||
this.goTo(FilterQueryParam.Penciller, filter);
|
||||
this.goTo({field: FilterField.Penciller, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Publisher:
|
||||
this.goTo(FilterQueryParam.Publisher, filter);
|
||||
this.goTo({field: FilterField.Publisher, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Translator:
|
||||
this.goTo(FilterQueryParam.Translator, filter);
|
||||
this.goTo({field: FilterField.Translators, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -232,4 +242,6 @@ export class NavHeaderComponent implements OnInit {
|
|||
hideSideNav() {
|
||||
this.navService.toggleSideNav();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="row g-0">
|
||||
<div class="col-auto custom-col clickable" [ngbPopover]="popContent"
|
||||
popoverTitle="Your Rating + Overall" popoverClass="md-popover">
|
||||
popoverTitle="Your Rating + Overall" [popoverClass]="utilityService.getActiveBreakpoint() > Breakpoint.Mobile ? 'md-popover' : 'lg-popover'">
|
||||
<span class="badge rounded-pill ps-0 me-1">
|
||||
<img class="me-1" ngSrc="assets/images/logo-32.png" width="24" height="24" alt="">
|
||||
<ng-container *ngIf="hasUserRated; else notYetRated">{{userRating * 20}}</ng-container>
|
||||
|
@ -23,7 +23,7 @@
|
|||
</div>
|
||||
|
||||
<ng-template #popContent>
|
||||
<ngx-stars [initialStars]="userRating" (ratingOutput)="updateRating($event)"
|
||||
<ngx-stars [initialStars]="userRating" (ratingOutput)="updateRating($event)" [size]="utilityService.getActiveBreakpoint() > Breakpoint.Mobile ? 1 : 2"
|
||||
[maxStars]="5" [color]="starColor"></ngx-stars>
|
||||
{{userRating * 20}}%
|
||||
</ng-template>
|
||||
|
|
|
@ -19,6 +19,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.lg-popover {
|
||||
width: 320px;
|
||||
|
||||
> .popover-body {
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.rating-star {
|
||||
i {
|
||||
position: relative;
|
||||
|
|
|
@ -18,6 +18,7 @@ import {LibraryType} from "../../../_models/library";
|
|||
import {ProviderNamePipe} from "../../../pipe/provider-name.pipe";
|
||||
import {NgxStarsModule} from "ngx-stars";
|
||||
import {ThemeService} from "../../../_services/theme.service";
|
||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-external-rating',
|
||||
|
@ -37,6 +38,7 @@ export class ExternalRatingComponent implements OnInit {
|
|||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly themeService = inject(ThemeService);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
|
||||
ratings: Array<Rating> = [];
|
||||
isLoading: boolean = false;
|
||||
|
@ -71,4 +73,6 @@ export class ExternalRatingComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@ import {CommonModule} from '@angular/common';
|
|||
import {A11yClickDirective} from "../../../shared/a11y-click.directive";
|
||||
import {BadgeExpanderComponent} from "../../../shared/badge-expander/badge-expander.component";
|
||||
import {TagBadgeComponent, TagBadgeCursor} from "../../../shared/tag-badge/tag-badge.component";
|
||||
import {FilterQueryParam, FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
|
||||
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
|
||||
import {FilterField} from "../../../_models/metadata/v2/filter-field";
|
||||
|
||||
|
@ -25,7 +24,6 @@ export class MetadataDetailComponent {
|
|||
@ContentChild('titleTemplate') titleTemplate!: TemplateRef<any>;
|
||||
@ContentChild('itemTemplate') itemTemplate?: TemplateRef<any>;
|
||||
|
||||
private readonly router = inject(Router);
|
||||
private readonly filterUtilitiesService = inject(FilterUtilitiesService);
|
||||
protected readonly TagBadgeCursor = TagBadgeCursor;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {ActivatedRouteSnapshot, Router} from '@angular/router';
|
||||
import {ActivatedRouteSnapshot, Params, Router} from '@angular/router';
|
||||
import {Pagination} from 'src/app/_models/pagination';
|
||||
import {SortField, SortOptions} from 'src/app/_models/metadata/series-filter';
|
||||
import {MetadataService} from "../../_services/metadata.service";
|
||||
|
@ -9,50 +9,17 @@ import {FilterCombination} from "../../_models/metadata/v2/filter-combination";
|
|||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
|
||||
|
||||
/**
|
||||
* Used to pass state between the filter and the url
|
||||
*/
|
||||
export enum FilterQueryParam {
|
||||
Format = 'format',
|
||||
Genres = 'genres',
|
||||
AgeRating = 'ageRating',
|
||||
PublicationStatus = 'publicationStatus',
|
||||
Tags = 'tags',
|
||||
Languages = 'languages',
|
||||
CollectionTags = 'collectionTags',
|
||||
Libraries = 'libraries',
|
||||
Writers = 'writers',
|
||||
Artists = 'artists',
|
||||
Character = 'character',
|
||||
Colorist = 'colorist',
|
||||
CoverArtists = 'coverArtists',
|
||||
Editor = 'editor',
|
||||
Inker = 'inker',
|
||||
Letterer = 'letterer',
|
||||
Penciller = 'penciller',
|
||||
Publisher = 'publisher',
|
||||
Translator = 'translators',
|
||||
ReadStatus = 'readStatus',
|
||||
SortBy = 'sortBy',
|
||||
Rating = 'rating',
|
||||
Name = 'name',
|
||||
/**
|
||||
* This is a pagination control
|
||||
*/
|
||||
Page = 'page',
|
||||
/**
|
||||
* Special case for the UI. Does not trigger filtering
|
||||
*/
|
||||
None = 'none'
|
||||
}
|
||||
const sortOptionsKey = 'sortOptions=';
|
||||
const statementsKey = 'stmts=';
|
||||
const limitToKey = 'limitTo=';
|
||||
const combinationKey = 'combination=';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FilterUtilitiesService {
|
||||
|
||||
constructor(private metadataService: MetadataService, private router: Router) {
|
||||
}
|
||||
constructor(private metadataService: MetadataService, private router: Router) {}
|
||||
|
||||
applyFilter(page: Array<any>, filter: FilterField, comparison: FilterComparison, value: string) {
|
||||
const dto: SeriesFilterV2 = {
|
||||
|
@ -61,8 +28,14 @@ export class FilterUtilitiesService {
|
|||
limitTo: 0
|
||||
};
|
||||
|
||||
console.log('applying filter: ', this.urlFromFilterV2(page.join('/') + '?', dto))
|
||||
this.router.navigateByUrl(this.urlFromFilterV2(page.join('/') + '?', dto));
|
||||
const url = this.urlFromFilterV2(page.join('/') + '?', dto);
|
||||
return this.router.navigateByUrl(url);
|
||||
}
|
||||
|
||||
applyFilterWithParams(page: Array<any>, filter: SeriesFilterV2, extraParams: Params) {
|
||||
let url = this.urlFromFilterV2(page.join('/') + '?', filter);
|
||||
url += Object.keys(extraParams).map(k => `&${k}=${extraParams[k]}`).join('');
|
||||
return this.router.navigateByUrl(url, extraParams);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,7 +57,7 @@ export class FilterUtilitiesService {
|
|||
|
||||
/**
|
||||
* Will fetch current page from route if present
|
||||
* @param ActivatedRouteSnapshot to fetch page from. Must be from component else may get stale data
|
||||
* @param snapshot to fetch page from. Must be from component else may get stale data
|
||||
* @param itemsPerPage If you want pagination, pass non-zero number
|
||||
* @returns A default pagination object
|
||||
*/
|
||||
|
@ -107,10 +80,10 @@ export class FilterUtilitiesService {
|
|||
|
||||
encodeSeriesFilter(filter: SeriesFilterV2) {
|
||||
const encodedStatements = this.encodeFilterStatements(filter.statements);
|
||||
const encodedSortOptions = filter.sortOptions ? `sortOptions=${this.encodeSortOptions(filter.sortOptions)}` : '';
|
||||
const encodedLimitTo = `limitTo=${filter.limitTo}`;
|
||||
const encodedSortOptions = filter.sortOptions ? `${sortOptionsKey}${this.encodeSortOptions(filter.sortOptions)}` : '';
|
||||
const encodedLimitTo = `${limitToKey}${filter.limitTo}`;
|
||||
|
||||
return `${this.encodeName(filter.name)}stmts=${encodedStatements}&${encodedSortOptions}&${encodedLimitTo}&combination=${filter.combination}`;
|
||||
return `${this.encodeName(filter.name)}${encodedStatements}&${encodedSortOptions}&${encodedLimitTo}&${combinationKey}${filter.combination}`;
|
||||
}
|
||||
|
||||
encodeName(name: string | undefined) {
|
||||
|
@ -124,7 +97,8 @@ export class FilterUtilitiesService {
|
|||
}
|
||||
|
||||
encodeFilterStatements(statements: Array<FilterStatement>) {
|
||||
return encodeURIComponent(statements.map(statement => {
|
||||
if (statements.length === 0) return '';
|
||||
return statementsKey + encodeURIComponent(statements.map(statement => {
|
||||
const encodedComparison = `comparison=${statement.comparison}`;
|
||||
const encodedField = `field=${statement.field}`;
|
||||
const encodedValue = `value=${encodeURIComponent(statement.value)}`;
|
||||
|
@ -144,19 +118,23 @@ export class FilterUtilitiesService {
|
|||
}
|
||||
|
||||
const fullUrl = window.location.href.split('?')[1];
|
||||
const stmtsStartIndex = fullUrl.indexOf('stmts=');
|
||||
let endIndex = fullUrl.indexOf('&sortOptions=');
|
||||
const stmtsStartIndex = fullUrl.indexOf(statementsKey);
|
||||
let endIndex = fullUrl.indexOf('&' + sortOptionsKey);
|
||||
if (endIndex < 0) {
|
||||
endIndex = fullUrl.indexOf('&limitTo=');
|
||||
endIndex = fullUrl.indexOf('&' + limitToKey);
|
||||
}
|
||||
|
||||
if (stmtsStartIndex !== -1 && endIndex !== -1) {
|
||||
const stmtsEncoded = fullUrl.substring(stmtsStartIndex + 6, endIndex);
|
||||
if (stmtsStartIndex !== -1 || endIndex !== -1) {
|
||||
// +1 is for the =
|
||||
const stmtsEncoded = fullUrl.substring(stmtsStartIndex + statementsKey.length, endIndex);
|
||||
filter.statements = this.decodeFilterStatements(stmtsEncoded);
|
||||
}
|
||||
|
||||
if (queryParams.sortOptions) {
|
||||
const sortOptions = this.decodeSortOptions(queryParams.sortOptions);
|
||||
const optionsStartIndex = fullUrl.indexOf('&' + sortOptionsKey);
|
||||
const endIndex = fullUrl.indexOf('&' + limitToKey);
|
||||
const sortOptionsEncoded = fullUrl.substring(optionsStartIndex + sortOptionsKey.length + 1, endIndex);
|
||||
const sortOptions = this.decodeSortOptions(sortOptionsEncoded);
|
||||
if (sortOptions) {
|
||||
filter.sortOptions = sortOptions;
|
||||
}
|
||||
|
@ -174,7 +152,7 @@ export class FilterUtilitiesService {
|
|||
}
|
||||
|
||||
decodeSortOptions(encodedSortOptions: string): SortOptions | null {
|
||||
const parts = encodedSortOptions.split('&');
|
||||
const parts = decodeURIComponent(encodedSortOptions).split('&');
|
||||
const sortFieldPart = parts.find(part => part.startsWith('sortField='));
|
||||
const isAscendingPart = parts.find(part => part.startsWith('isAscending='));
|
||||
|
||||
|
@ -188,7 +166,7 @@ export class FilterUtilitiesService {
|
|||
}
|
||||
|
||||
decodeFilterStatements(encodedStatements: string): FilterStatement[] {
|
||||
const statementStrings = decodeURIComponent(encodedStatements).split(','); // I don't think this will wrk
|
||||
const statementStrings = decodeURIComponent(encodedStatements).split(',');
|
||||
return statementStrings.map(statementString => {
|
||||
const parts = statementString.split('&');
|
||||
if (parts === null || parts.length < 3) return null;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {ChangeDetectionStrategy, Component, DestroyRef, inject} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
|
||||
import {FormControl} from '@angular/forms';
|
||||
import { BarChartModule } from '@swimlane/ngx-charts';
|
||||
import {map, Observable} from 'rxjs';
|
||||
|
@ -18,8 +18,9 @@ import {TranslocoDirective} from "@ngneat/transloco";
|
|||
standalone: true,
|
||||
imports: [BarChartModule, AsyncPipe, TranslocoDirective]
|
||||
})
|
||||
export class DayBreakdownComponent {
|
||||
export class DayBreakdownComponent implements OnInit {
|
||||
|
||||
@Input() userId = 0;
|
||||
view: [number, number] = [0,0];
|
||||
showLegend: boolean = true;
|
||||
|
||||
|
@ -27,9 +28,11 @@ export class DayBreakdownComponent {
|
|||
dayBreakdown$!: Observable<Array<PieDataItem>>;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(private statService: StatisticsService) {
|
||||
constructor(private statService: StatisticsService) {}
|
||||
|
||||
ngOnInit() {
|
||||
const dayOfWeekPipe = new DayOfWeekPipe();
|
||||
this.dayBreakdown$ = this.statService.getDayBreakdown().pipe(
|
||||
this.dayBreakdown$ = this.statService.getDayBreakdown(this.userId).pipe(
|
||||
map((data: Array<StatCount<DayOfWeek>>) => {
|
||||
return data.map(d => {
|
||||
return {name: dayOfWeekPipe.transform(d.value), value: d.count};
|
||||
|
|
|
@ -2,7 +2,7 @@ import {ChangeDetectionStrategy, Component, DestroyRef, HostListener, inject} fr
|
|||
import {Router} from '@angular/router';
|
||||
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {map, Observable, ReplaySubject, shareReplay} from 'rxjs';
|
||||
import {FilterQueryParam, FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
|
||||
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
|
||||
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {Series} from 'src/app/_models/series';
|
||||
import {ImageService} from 'src/app/_services/image.service';
|
||||
|
|
|
@ -14,6 +14,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2" style="height: 242px">
|
||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||
<app-day-breakdown [userId]="userId"></app-day-breakdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2 " style="height: 242px">
|
||||
<app-stat-list [data$]="percentageRead$" [label]="t('read-percentage')" [title]="t('library-read-progress-title')"></app-stat-list>
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,7 @@ import {StatListComponent} from '../stat-list/stat-list.component';
|
|||
import {ReadingActivityComponent} from '../reading-activity/reading-activity.component';
|
||||
import {UserStatsInfoCardsComponent} from '../user-stats-info-cards/user-stats-info-cards.component';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
import {DayBreakdownComponent} from "../day-breakdown/day-breakdown.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-stats',
|
||||
|
@ -27,6 +28,7 @@ import {TranslocoModule} from "@ngneat/transloco";
|
|||
StatListComponent,
|
||||
AsyncPipe,
|
||||
TranslocoModule,
|
||||
DayBreakdownComponent,
|
||||
],
|
||||
})
|
||||
export class UserStatsComponent implements OnInit {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue