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:
Joe Milazzo 2023-08-15 16:33:39 -05:00 committed by GitHub
parent ef3e76e3e5
commit c84a3294e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 324 additions and 246 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,6 +19,14 @@
}
}
.lg-popover {
width: 320px;
> .popover-body {
padding-top: 0px;
}
}
.rating-star {
i {
position: relative;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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