Localization - First Pass (#2174)
* Started designing the backend localization service * Worked in Transloco for initial PoC * Worked in Transloco for initial PoC * Translated the login screen * translated dashboard screen * Started work on the backend * Fixed a logic bug * translated edit-user screen * Hooked up the backend for having a locale property. * Hooked up the ability to view the available locales and switch to them. * Made the localization service languages be derived from what's in langs/ directory. * Fixed up localization switching * Switched when we check for a license on UI bootstrap * Tweaked some code * Fixed the bug where dashboard wasn't loading and made it so language switching is working. * Fixed a bug on dashboard with languagePath * Converted user-scrobble-history.component.html * Converted spoiler.component.html * Converted review-series-modal.component.html * Converted review-card-modal.component.html * Updated the readme * Translated using Weblate (English) Currently translated at 100.0% (54 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/en/ * Converted review-card.component.html * Deleted dead component * Converted want-to-read.component.html * Added translation using Weblate (Korean) * Translated using Weblate (Spanish) Currently translated at 40.7% (22 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/ * Translated using Weblate (Korean) Currently translated at 62.9% (34 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-preferences.component.html * Translated using Weblate (Korean) Currently translated at 92.5% (50 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-holds.component.html * Converted theme-manager.component.html * Converted restriction-selector.component.html * Converted manage-devices.component.html * Converted edit-device.component.html * Converted change-password.component.html * Converted change-email.component.html * Converted change-age-restriction.component.html * Converted api-key.component.html * Converted anilist-key.component.html * Converted typeahead.component.html * Converted user-stats-info-cards.component.html * Converted user-stats.component.html * Converted top-readers.component.html * Converted some pipes and ensure translation is loaded before the app. * Finished all but one pipe for localization * Converted directory-picker.component.html * Converted library-access-modal.component.html * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Merged weblate in * ... -> … update * Updated the readme * Updateded all fonts to be woff2 * Cleaned up some strings to increase re-use * Removed an old flow (that doesn't exist in backend any longer) from when we introduced emails on Kavita. * Converted Series detail * Lots more converted * Lots more converted & hooked up the ability to flatten during prod build the language files. * Lots more converted * Lots more converted & fixed a bunch of broken pipes due to inject() * Lots more converted * Lots more converted * Lots more converted & fixed some bad keys * Lots more converted * Fixed some bugs with admin dasbhoard nested tabs not rendering on first load due to not using onpush change detection * Fixed up some localization errors and fixed forgot password error when the user doesn't have change password permission * Fixed a stupid build issue again * Started adding errors for interceptor and backend. * Finished off manga-reader * More translations * Few fixes * Fixed a bug where character tag badges weren't showing the name on chapter info * All components are translated * All toasts are translated * All confirm/alerts are translated * Trying something new for the backend * Migrated the localization strings for the backend into a new file. * Updated the localization service to be able to do backend localization with fallback to english. * Cleaned up some external reviews code to reduce looping * Localized AccountController.cs * 60% done with controllers * All controllers are done * All KavitaExceptions are covered * Some shakeout fixes * Prep for initial merge * Everything is done except options and basic shakeout proves response times are good. Unit tests are broken. * Fixed up the unit tests * All unit tests are now working * Removed some quantifier * I'm not sure I can support localization for some Volume/Chapter/Book strings within the codebase. --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: majora2007 <kavitareader@gmail.com> Co-authored-by: expertjun <jtrobin@naver.com> Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
This commit is contained in:
parent
670bf82c38
commit
3b23d63234
389 changed files with 13652 additions and 7925 deletions
|
|
@ -1,28 +1,30 @@
|
|||
|
||||
<div class="modal-header">
|
||||
<ng-container *transloco="let t; read:'generic-list-modal'">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form style="width: 100%" [formGroup]="listForm">
|
||||
<div class="mb-3" *ngIf="items.length >= 5">
|
||||
<label for="filter" class="form-label">Filter</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<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" *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>
|
||||
</li>
|
||||
<div class="mb-3" *ngIf="items.length >= 5">
|
||||
<label for="filter" class="form-label">{{t('filter')}}</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">{{t('clear')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<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" *ngIf="clicked !== undefined" (click)="handleClick(item)">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('open-filtered-search',{item: item})}}</span>
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="close()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="close()">{{t('close')}}</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { FilterPipe } from '../../../../pipe/filter.pipe';
|
||||
import { NgIf, NgFor } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-generic-list-modal',
|
||||
templateUrl: './generic-list-modal.component.html',
|
||||
styleUrls: ['./generic-list-modal.component.scss'],
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgIf, NgFor, FilterPipe]
|
||||
imports: [ReactiveFormsModule, NgIf, NgFor, FilterPipe, TranslocoModule]
|
||||
})
|
||||
export class GenericListModalComponent {
|
||||
@Input() items: Array<string> = [];
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
<div class="dashboard-card-content">
|
||||
<div class="row g-0 mb-2">
|
||||
<h4>Day Breakdown</h4>
|
||||
</div>
|
||||
<ng-container *transloco="let t; read: 'day-breakdown'">
|
||||
<div class="dashboard-card-content">
|
||||
<div class="row g-0 mb-2">
|
||||
<h4>{{t('title')}}</h4>
|
||||
</div>
|
||||
|
||||
<ngx-charts-bar-vertical
|
||||
class="dark"
|
||||
[results]="dayBreakdown$ | async"
|
||||
[xAxis]="true"
|
||||
[yAxis]="true"
|
||||
[legend]="showLegend"
|
||||
[showXAxisLabel]="true"
|
||||
[showYAxisLabel]="true"
|
||||
xAxisLabel="Day of Week"
|
||||
yAxisLabel="Reading Events">
|
||||
</ngx-charts-bar-vertical>
|
||||
</div>
|
||||
<ngx-charts-bar-vertical
|
||||
class="dark"
|
||||
[results]="dayBreakdown$ | async"
|
||||
[xAxis]="true"
|
||||
[yAxis]="true"
|
||||
[legend]="showLegend"
|
||||
[showXAxisLabel]="true"
|
||||
[showYAxisLabel]="true"
|
||||
[xAxisLabel]="t('x-axis-label')"
|
||||
[yAxisLabel]="t('y-axis-label')">
|
||||
</ngx-charts-bar-vertical>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {ChangeDetectionStrategy, Component, DestroyRef, inject} from '@angular/core';
|
||||
import {FormControl} from '@angular/forms';
|
||||
import { LegendPosition, BarChartModule } from '@swimlane/ngx-charts';
|
||||
import { BarChartModule } from '@swimlane/ngx-charts';
|
||||
import {map, Observable} from 'rxjs';
|
||||
import {DayOfWeek, StatisticsService} from 'src/app/_services/statistics.service';
|
||||
import {PieDataItem} from '../../_models/pie-data-item';
|
||||
|
|
@ -8,6 +8,7 @@ import {StatCount} from '../../_models/stat-count';
|
|||
import {DayOfWeekPipe} from '../../_pipes/day-of-week.pipe';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-day-breakdown',
|
||||
|
|
@ -15,19 +16,12 @@ import { AsyncPipe } from '@angular/common';
|
|||
styleUrls: ['./day-breakdown.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [BarChartModule, AsyncPipe]
|
||||
imports: [BarChartModule, AsyncPipe, TranslocoModule]
|
||||
})
|
||||
export class DayBreakdownComponent {
|
||||
|
||||
view: [number, number] = [0,0];
|
||||
gradient: boolean = true;
|
||||
showLegend: boolean = true;
|
||||
showLabels: boolean = true;
|
||||
isDoughnut: boolean = false;
|
||||
legendPosition: LegendPosition = LegendPosition.Right;
|
||||
colorScheme = {
|
||||
domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA']
|
||||
};
|
||||
|
||||
formControl: FormControl = new FormControl(true, []);
|
||||
dayBreakdown$!: Observable<Array<PieDataItem>>;
|
||||
|
|
|
|||
|
|
@ -1,74 +1,77 @@
|
|||
<div class="dashboard-card-content">
|
||||
<ng-container *transloco="let t; read: 'file-breakdown-stats'">
|
||||
<div class="dashboard-card-content">
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-8">
|
||||
<h4><span>Format</span>
|
||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0"></i>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<form>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input id="pub-file-breakdown-viz" type="checkbox" class="form-check-input" [formControl]="formControl" role="switch">
|
||||
<label for="pub-file-breakdown-viz" class="form-check-label">{{formControl.value ? 'Vizualization' : 'Data Table' }}</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<h4><span>{{t('format-title')}}</span>
|
||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0"></i>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<form>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input id="pub-file-breakdown-viz" type="checkbox" class="form-check-input" [formControl]="formControl" role="switch">
|
||||
<label for="pub-file-breakdown-viz" class="form-check-label">{{formControl.value ? t('visualisation-label') : t('data-table-label') }}</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #tooltip>Not Classified means Kavita has not scanned some files. This occurs on old files existing prior to v0.7. You may need to run a forced scan via Library settings modal.</ng-template>
|
||||
<ng-template #tooltip>{{t('format-tooltip')}}</ng-template>
|
||||
|
||||
|
||||
<ng-container *ngIf="files$ | async as files">
|
||||
<ng-container *ngIf="formControl.value; else tableLayout">
|
||||
<ngx-charts-advanced-pie-chart [results]="vizData2$ | async"></ngx-charts-advanced-pie-chart>
|
||||
</ng-container>
|
||||
<ng-template #tableLayout>
|
||||
<table class="table table-striped table-striped table-hover table-sm scrollable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" sortable="extension" (sort)="onSort($event)">
|
||||
Extension
|
||||
</th>
|
||||
<th scope="col" sortable="format" (sort)="onSort($event)">
|
||||
Format
|
||||
</th>
|
||||
<th scope="col" sortable="totalSize" (sort)="onSort($event)">
|
||||
Total Size
|
||||
</th>
|
||||
<th scope="col" sortable="totalFiles" (sort)="onSort($event)">
|
||||
Total Files
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of files; let idx = index;">
|
||||
<td id="adhoctask--{{idx}}">
|
||||
{{item.extension || 'Not Classified'}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.format | mangaFormat}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.totalSize | bytes}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.totalFiles | number:'1.0-0'}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>Total File Size:</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{((rawData$ | async)?.totalFileSize || 0) | bytes}}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="formControl.value; else tableLayout">
|
||||
<ngx-charts-advanced-pie-chart [results]="vizData2$ | async"></ngx-charts-advanced-pie-chart>
|
||||
</ng-container>
|
||||
<ng-template #tableLayout>
|
||||
<table class="table table-striped table-striped table-hover table-sm scrollable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" sortable="extension" (sort)="onSort($event)">
|
||||
{{t('extension-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="format" (sort)="onSort($event)">
|
||||
{{t('format-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="totalSize" (sort)="onSort($event)">
|
||||
{{t('total-size-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="totalFiles" (sort)="onSort($event)">
|
||||
{{t('total-files-header')}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of files; let idx = index;">
|
||||
<td id="adhoctask--{{idx}}">
|
||||
{{item.extension || t('not-classified')}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.format | mangaFormat}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.totalSize | bytes}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.totalFiles | number:'1.0-0'}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>{{t('total-file-size-title')}}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{((rawData$ | async)?.totalFileSize || 0) | bytes}}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ import {
|
|||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
OnDestroy,
|
||||
QueryList,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { LegendPosition, PieChartModule } from '@swimlane/ngx-charts';
|
||||
import { Observable, Subject, BehaviorSubject, combineLatest, map, takeUntil, shareReplay } from 'rxjs';
|
||||
import { Observable, BehaviorSubject, combineLatest, map, shareReplay } from 'rxjs';
|
||||
import { StatisticsService } from 'src/app/_services/statistics.service';
|
||||
import { SortableHeader, SortEvent, compare } from 'src/app/_single-module/table/_directives/sortable-header.directive';
|
||||
import { FileExtension, FileExtensionBreakdown } from '../../_models/file-breakdown';
|
||||
|
|
@ -20,6 +19,7 @@ import { BytesPipe } from '../../../pipe/bytes.pipe';
|
|||
import { SortableHeader as SortableHeader_1 } from '../../../_single-module/table/_directives/sortable-header.directive';
|
||||
import { NgIf, NgFor, AsyncPipe, DecimalPipe } from '@angular/common';
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
export interface StackedBarChartDataItem {
|
||||
name: string,
|
||||
|
|
@ -32,7 +32,7 @@ export interface StackedBarChartDataItem {
|
|||
styleUrls: ['./file-breakdown-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgbTooltip, ReactiveFormsModule, NgIf, PieChartModule, SortableHeader_1, NgFor, AsyncPipe, DecimalPipe, BytesPipe, MangaFormatPipe]
|
||||
imports: [NgbTooltip, ReactiveFormsModule, NgIf, PieChartModule, SortableHeader_1, NgFor, AsyncPipe, DecimalPipe, BytesPipe, MangaFormatPipe, TranslocoModule]
|
||||
})
|
||||
export class FileBreakdownStatsComponent {
|
||||
|
||||
|
|
@ -40,7 +40,6 @@ export class FileBreakdownStatsComponent {
|
|||
|
||||
rawData$!: Observable<FileExtensionBreakdown>;
|
||||
files$!: Observable<Array<FileExtension>>;
|
||||
vizData$!: Observable<Array<StackedBarChartDataItem>>;
|
||||
vizData2$!: Observable<Array<PieDataItem>>;
|
||||
|
||||
currentSort = new BehaviorSubject<SortEvent<FileExtension>>({column: 'extension', direction: 'asc'});
|
||||
|
|
@ -49,19 +48,11 @@ export class FileBreakdownStatsComponent {
|
|||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
view: [number, number] = [700, 400];
|
||||
gradient: boolean = true;
|
||||
showLegend: boolean = true;
|
||||
showLabels: boolean = true;
|
||||
isDoughnut: boolean = false;
|
||||
legendPosition: LegendPosition = LegendPosition.Right;
|
||||
colorScheme = {
|
||||
domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA']
|
||||
};
|
||||
|
||||
formControl: FormControl = new FormControl(true, []);
|
||||
|
||||
|
||||
constructor(private statService: StatisticsService) {
|
||||
constructor(private statService: StatisticsService, private translocoService: TranslocoService) {
|
||||
this.rawData$ = this.statService.getFileBreakdown().pipe(takeUntilDestroyed(this.destroyRef), shareReplay());
|
||||
|
||||
this.files$ = combineLatest([this.currentSort$, this.rawData$]).pipe(
|
||||
|
|
@ -80,7 +71,7 @@ export class FileBreakdownStatsComponent {
|
|||
|
||||
|
||||
this.vizData2$ = this.files$.pipe(takeUntilDestroyed(this.destroyRef), map(data => data.map(d => {
|
||||
return {name: d.extension || 'Not Categorized', value: d.totalFiles, extra: d.totalSize};
|
||||
return {name: d.extension || this.translocoService.translate('file-breakdown-stats.not-classified'), value: d.totalFiles, extra: d.totalSize};
|
||||
})));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,58 +1,56 @@
|
|||
<div class="row g-0 mb-2">
|
||||
<ng-container *transloco="let t; read: 'manga-format-stats'">
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-8">
|
||||
<h4><span>Format</span>
|
||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0"></i>
|
||||
</h4>
|
||||
<h4><span>{{t('title')}}</span></h4>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<form>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input id="manga-format-viz" type="checkbox" class="form-check-input" [formControl]="formControl" role="switch">
|
||||
<label for="manga-format-viz" class="form-check-label">{{formControl.value ? 'Vizualization' : 'Data Table' }}</label>
|
||||
</div>
|
||||
</form>
|
||||
<form>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input id="manga-format-viz" type="checkbox" class="form-check-input" [formControl]="formControl" role="switch">
|
||||
<label for="manga-format-viz" class="form-check-label">{{formControl.value ? t('visualisation-label') : t('data-table-label') }}</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #tooltip></ng-template>
|
||||
|
||||
|
||||
<ng-container *ngIf="formats$ | async as formats">
|
||||
<ng-container *ngIf="formats$ | async as formats">
|
||||
<ng-container *ngIf="formControl.value; else tableLayout">
|
||||
<ngx-charts-pie-chart
|
||||
<ngx-charts-pie-chart
|
||||
[view]="view"
|
||||
[results]="formats"
|
||||
[legend]="showLegend"
|
||||
[legendPosition]="legendPosition"
|
||||
[labels]="showLabels"
|
||||
>
|
||||
</ngx-charts-pie-chart>
|
||||
>
|
||||
</ngx-charts-pie-chart>
|
||||
</ng-container>
|
||||
<ng-template #tableLayout>
|
||||
<table class="table table-striped table-striped table-hover table-sm scrollable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" sortable="name" (sort)="onSort($event)">
|
||||
Format
|
||||
</th>
|
||||
<th scope="col" sortable="value" (sort)="onSort($event)">
|
||||
Count
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of formats; let idx = index;">
|
||||
<td id="adhoctask--{{idx}}">
|
||||
{{item.name}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.value | number:'1.0-0'}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table table-striped table-striped table-hover table-sm scrollable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" sortable="name" (sort)="onSort($event)">
|
||||
{{t('format-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="value" (sort)="onSort($event)">
|
||||
{{t('count-header')}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of formats; let idx = index;">
|
||||
<td id="adhoctask--{{idx}}">
|
||||
{{item.name}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.value | number:'1.0-0'}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ import {
|
|||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
QueryList,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { LegendPosition, PieChartModule } from '@swimlane/ngx-charts';
|
||||
import { Observable, Subject, BehaviorSubject, combineLatest, map, takeUntil } from 'rxjs';
|
||||
import { Observable, BehaviorSubject, combineLatest, map } from 'rxjs';
|
||||
import { StatisticsService } from 'src/app/_services/statistics.service';
|
||||
import { compare, SortableHeader, SortEvent } from 'src/app/_single-module/table/_directives/sortable-header.directive';
|
||||
import { PieDataItem } from '../../_models/pie-data-item';
|
||||
|
|
@ -18,6 +16,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||
import { SortableHeader as SortableHeader_1 } from '../../../_single-module/table/_directives/sortable-header.directive';
|
||||
import { NgIf, NgFor, AsyncPipe, DecimalPipe } from '@angular/common';
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manga-format-stats',
|
||||
|
|
@ -25,7 +24,7 @@ import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
|||
styleUrls: ['./manga-format-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgbTooltip, ReactiveFormsModule, NgIf, PieChartModule, SortableHeader_1, NgFor, AsyncPipe, DecimalPipe]
|
||||
imports: [NgbTooltip, ReactiveFormsModule, NgIf, PieChartModule, SortableHeader_1, NgFor, AsyncPipe, DecimalPipe, TranslocoModule]
|
||||
})
|
||||
export class MangaFormatStatsComponent {
|
||||
|
||||
|
|
@ -38,14 +37,9 @@ export class MangaFormatStatsComponent {
|
|||
currentSort$: Observable<SortEvent<PieDataItem>> = this.currentSort.asObservable();
|
||||
|
||||
view: [number, number] = [700, 400];
|
||||
gradient: boolean = true;
|
||||
showLegend: boolean = true;
|
||||
showLabels: boolean = true;
|
||||
isDoughnut: boolean = false;
|
||||
legendPosition: LegendPosition = LegendPosition.Right;
|
||||
colorScheme = {
|
||||
domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA']
|
||||
};
|
||||
|
||||
formControl: FormControl = new FormControl(true, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,54 +1,57 @@
|
|||
<div class="dashboard-card-content">
|
||||
<ng-container *transloco="let t; read: 'publication-status-stats'">
|
||||
<div class="dashboard-card-content">
|
||||
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-8">
|
||||
<h4><span>Publication Status</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<form>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input id="pub-status-viz" type="checkbox" class="form-check-input" [formControl]="formControl" role="switch">
|
||||
<label for="pub-status-viz" class="form-check-label">{{formControl.value ? 'Vizualization' : 'Data Table' }}</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<h4><span>{{t('title')}}</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<form>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input id="pub-status-viz" type="checkbox" class="form-check-input" [formControl]="formControl" role="switch">
|
||||
<label for="pub-status-viz" class="form-check-label">{{formControl.value ? t('visualisation-label') : t('data-table-label') }}</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-container *ngIf="publicationStatues$ | async as statuses">
|
||||
<ng-container *ngIf="formControl.value; else tableLayout">
|
||||
<ngx-charts-advanced-pie-chart
|
||||
[results]="statuses"
|
||||
>
|
||||
</ngx-charts-advanced-pie-chart>
|
||||
</ng-container>
|
||||
<ng-template #tableLayout>
|
||||
<table class="table table-striped table-hover table-striped table-sm scrollable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" sortable="name" (sort)="onSort($event)">
|
||||
Year
|
||||
</th>
|
||||
<th scope="col" sortable="value" (sort)="onSort($event)">
|
||||
Count
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of statuses; let idx = index;">
|
||||
<td id="adhoctask--{{idx}}">
|
||||
{{item.name}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.value | number:'1.0-0'}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="formControl.value; else tableLayout">
|
||||
<ngx-charts-advanced-pie-chart
|
||||
[results]="statuses"
|
||||
>
|
||||
</ngx-charts-advanced-pie-chart>
|
||||
</ng-container>
|
||||
<ng-template #tableLayout>
|
||||
<table class="table table-striped table-hover table-striped table-sm scrollable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" sortable="name" (sort)="onSort($event)">
|
||||
{{t('year-header')}}
|
||||
</th>
|
||||
<th scope="col" sortable="value" (sort)="onSort($event)">
|
||||
{{t('count-header')}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of statuses; let idx = index;">
|
||||
<td id="adhoctask--{{idx}}">
|
||||
{{item.name}}
|
||||
</td>
|
||||
<td>
|
||||
{{item.value | number:'1.0-0'}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@ import {
|
|||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
OnDestroy,
|
||||
QueryList,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { LegendPosition, PieChartModule } from '@swimlane/ngx-charts';
|
||||
import { Observable, Subject, map, takeUntil, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
import { Observable, map, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
import { StatisticsService } from 'src/app/_services/statistics.service';
|
||||
import { compare, SortableHeader, SortEvent } from 'src/app/_single-module/table/_directives/sortable-header.directive';
|
||||
import { PieDataItem } from '../../_models/pie-data-item';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { SortableHeader as SortableHeader_1 } from '../../../_single-module/table/_directives/sortable-header.directive';
|
||||
import { NgIf, NgFor, AsyncPipe, DecimalPipe } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-publication-status-stats',
|
||||
|
|
@ -23,7 +23,7 @@ import { NgIf, NgFor, AsyncPipe, DecimalPipe } from '@angular/common';
|
|||
styleUrls: ['./publication-status-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgIf, PieChartModule, SortableHeader_1, NgFor, AsyncPipe, DecimalPipe]
|
||||
imports: [ReactiveFormsModule, NgIf, PieChartModule, SortableHeader_1, NgFor, AsyncPipe, DecimalPipe, TranslocoModule]
|
||||
})
|
||||
export class PublicationStatusStatsComponent {
|
||||
|
||||
|
|
@ -35,14 +35,6 @@ export class PublicationStatusStatsComponent {
|
|||
currentSort$: Observable<SortEvent<PieDataItem>> = this.currentSort.asObservable();
|
||||
|
||||
view: [number, number] = [700, 400];
|
||||
gradient: boolean = true;
|
||||
showLegend: boolean = true;
|
||||
showLabels: boolean = true;
|
||||
isDoughnut: boolean = false;
|
||||
legendPosition: LegendPosition = LegendPosition.Right;
|
||||
colorScheme = {
|
||||
domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA']
|
||||
};
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,54 +1,58 @@
|
|||
<ng-container>
|
||||
<ng-container *transloco="let t; read: 'reading-activity'">
|
||||
<ng-container>
|
||||
<div class="dashboard-card-content">
|
||||
<div class="row g-0 mb-2 align-items-center">
|
||||
<div class="col-4">
|
||||
<h4>Reading Activity</h4>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<form [formGroup]="formGroup" class="d-inline-flex float-end">
|
||||
<div class="d-flex me-1" *ngIf="isAdmin && !individualUserMode">
|
||||
<label for="time-select-read-by-day" class="form-check-label"></label>
|
||||
<select id="time-select-read-by-day" class="form-select" formControlName="users"
|
||||
[class.is-invalid]="formGroup.get('users')?.invalid && formGroup.get('users')?.touched">
|
||||
<option [value]="0">All Users</option>
|
||||
<option *ngFor="let item of users$ | async" [value]="item.id">{{item.username}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<label for="time-select-top-reads" class="form-check-label"></label>
|
||||
<select id="time-select-top-reads" class="form-select" formControlName="days"
|
||||
[class.is-invalid]="formGroup.get('days')?.invalid && formGroup.get('days')?.touched">
|
||||
<option *ngFor="let item of timePeriods" [value]="item.value">{{item.title}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row g-0 mb-2 align-items-center">
|
||||
<div class="col-4">
|
||||
<h4>{{t('reading-activity')}}</h4>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<ng-container *ngIf="data$ | async as data">
|
||||
<ngx-charts-line-chart
|
||||
*ngIf="data.length > 0; else noData"
|
||||
class="dark"
|
||||
[legend]="true"
|
||||
legendTitle="Formats"
|
||||
[showXAxisLabel]="true"
|
||||
[showYAxisLabel]="true"
|
||||
[xAxis]="true"
|
||||
[yAxis]="true"
|
||||
[showGridLines]="false"
|
||||
[showRefLines]="true"
|
||||
[roundDomains]="true"
|
||||
[autoScale]="true"
|
||||
xAxisLabel="Time"
|
||||
yAxisLabel="Hours Read"
|
||||
[timeline]="false"
|
||||
[results]="data"
|
||||
>
|
||||
</ngx-charts-line-chart>
|
||||
</ng-container>
|
||||
<div class="col-8">
|
||||
<form [formGroup]="formGroup" class="d-inline-flex float-end">
|
||||
<div class="d-flex me-1" *ngIf="isAdmin && !individualUserMode">
|
||||
<label for="time-select-read-by-day" class="form-check-label"></label>
|
||||
<select id="time-select-read-by-day" class="form-select" formControlName="users"
|
||||
[class.is-invalid]="formGroup.get('users')?.invalid && formGroup.get('users')?.touched">
|
||||
<option [value]="0">All Users</option>
|
||||
<option *ngFor="let item of users$ | async" [value]="item.id">{{item.username}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<label for="time-select-top-reads" class="form-check-label">
|
||||
<span class="visually-hidden">{{t('time-frame-label')}}</span>
|
||||
</label>
|
||||
<select id="time-select-top-reads" class="form-select" formControlName="days"
|
||||
[class.is-invalid]="formGroup.get('days')?.invalid && formGroup.get('days')?.touched">
|
||||
<option *ngFor="let item of timePeriods" [value]="item.value">{{t(item.title)}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #noData>
|
||||
No Reading progress
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<ng-container *ngIf="data$ | async as data">
|
||||
<ngx-charts-line-chart
|
||||
*ngIf="data.length > 0; else noData"
|
||||
class="dark"
|
||||
[legend]="true"
|
||||
[legendTitle]="t('legend-label')"
|
||||
[showXAxisLabel]="true"
|
||||
[showYAxisLabel]="true"
|
||||
[xAxis]="true"
|
||||
[yAxis]="true"
|
||||
[showGridLines]="false"
|
||||
[showRefLines]="true"
|
||||
[roundDomains]="true"
|
||||
[autoScale]="true"
|
||||
[xAxisLabel]="t('x-axis-label')"
|
||||
[yAxisLabel]="t('y-axis-label')"
|
||||
[timeline]="false"
|
||||
[results]="data"
|
||||
>
|
||||
</ngx-charts-line-chart>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #noData>
|
||||
{{t('no-data')}}
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import { TimePeriods } from '../top-readers/top-readers.component';
|
|||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { LineChartModule } from '@swimlane/ngx-charts';
|
||||
import { NgIf, NgFor, AsyncPipe } from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" };
|
||||
const mangaFormatPipe = new MangaFormatPipe();
|
||||
|
||||
@Component({
|
||||
selector: 'app-reading-activity',
|
||||
|
|
@ -20,7 +20,7 @@ const mangaFormatPipe = new MangaFormatPipe();
|
|||
styleUrls: ['./reading-activity.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgIf, NgFor, LineChartModule, AsyncPipe]
|
||||
imports: [ReactiveFormsModule, NgIf, NgFor, LineChartModule, AsyncPipe, TranslocoModule]
|
||||
})
|
||||
export class ReadingActivityComponent implements OnInit {
|
||||
/**
|
||||
|
|
@ -39,13 +39,15 @@ export class ReadingActivityComponent implements OnInit {
|
|||
data$: Observable<Array<PieDataItem>>;
|
||||
timePeriods = TimePeriods;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly translocoService = inject(TranslocoService);
|
||||
mangaFormatPipe = new MangaFormatPipe(this.translocoService);
|
||||
|
||||
constructor(private statService: StatisticsService, private memberService: MemberService) {
|
||||
this.data$ = this.formGroup.valueChanges.pipe(
|
||||
switchMap(_ => this.statService.getReadCountByDay(this.formGroup.get('users')!.value, this.formGroup.get('days')!.value)),
|
||||
map(data => {
|
||||
const gList = data.reduce((formats, entry) => {
|
||||
const formatTranslated = mangaFormatPipe.transform(entry.format);
|
||||
const formatTranslated = this.mangaFormatPipe.transform(entry.format);
|
||||
if (!formats[formatTranslated]) {
|
||||
formats[formatTranslated] = {
|
||||
name: formatTranslated,
|
||||
|
|
|
|||
|
|
@ -1,123 +1,124 @@
|
|||
<div class="container-fluid">
|
||||
<ng-container *transloco="let t; read:'server-stats'">
|
||||
<div class="container-fluid">
|
||||
<div class="row g-0 mt-4 mb-3 d-flex justify-content-around" *ngIf="stats$ | async as stats">
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Series" [clickable]="false" fontClasses="fa-solid fa-book-open" title="Total Series: {{stats.seriesCount | number}}">
|
||||
{{stats.seriesCount | compactNumber}} Series
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container >
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Volumes" [clickable]="false" fontClasses="fas fa-book" title="Total Volumes: {{stats.volumeCount | number}}">
|
||||
{{stats.volumeCount | compactNumber}} Volumes
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-series-label')" [clickable]="false" fontClasses="fa-solid fa-book-open" [title]="t('total-series-tooltip', {count: stats.seriesCount | number})">
|
||||
{{stats.seriesCount | compactNumber}} Series
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Files" [clickable]="false" fontClasses="fa-regular fa-file" title="Total Files: {{stats.totalFiles | number}}">
|
||||
{{stats.totalFiles | compactNumber}} Files
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Size" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" title="Total Size">
|
||||
{{stats.totalSize | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<ng-container >
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-volumes-label')" [clickable]="false" fontClasses="fas fa-book" [title]="t('total-volumes-tooltip', {count: stats.volumeCount | number})">
|
||||
{{stats.volumeCount | compactNumber}} Volumes
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Genres" [clickable]="true" fontClasses="fa-solid fa-tags" title="Total Genres: {{stats.totalGenres | number}}" (click)="openGenreList();$event.stopPropagation();">
|
||||
{{stats.totalGenres | compactNumber}} Genres
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-files-label')" [clickable]="false" fontClasses="fa-regular fa-file" [title]="t('total-files-tooltip', {count: stats.totalFiles | number})">
|
||||
{{stats.totalFiles | compactNumber}} Files
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Tags" [clickable]="true" fontClasses="fa-solid fa-tags" title="Total Tags: {{stats.totalTags | number}}" (click)="openTagList();$event.stopPropagation();">
|
||||
{{stats.totalTags | compactNumber}} Tags
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-size-label')" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" [title]="t('total-size-label')">
|
||||
{{stats.totalSize | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total People" [clickable]="true" fontClasses="fa-solid fa-user-tag" title="Total People: {{stats.totalPeople | number}}" (click)="openPeopleList();$event.stopPropagation();">
|
||||
{{stats.totalPeople | compactNumber}} People
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Genres" [clickable]="true" fontClasses="fa-solid fa-tags" [title]="t('total-genres-tooltip', {count: stats.totalGenres | number})" (click)="openGenreList();$event.stopPropagation();">
|
||||
{{t('genre-count', {num: stats.totalGenres | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Read Time" [clickable]="false" fontClasses="fas fa-eye" title="Total Read Time: {{stats.totalReadingTime | number}}">
|
||||
{{stats.totalReadingTime | timeDuration}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-tags-label')" [clickable]="true" fontClasses="fa-solid fa-tags" [title]="t('total-tags-tooltip', {count: stats.totalTags | number})" (click)="openTagList();$event.stopPropagation();">
|
||||
{{t('tag-count', {num: stats.totalTags | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-people-label')" [clickable]="true" fontClasses="fa-solid fa-user-tag" [title]="t('total-people-tooltip', {count: stats.totalPeople | number})" (click)="openPeopleList();$event.stopPropagation();">
|
||||
{{t('people-count', {num: stats.totalPeople | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-read-time-label')" [clickable]="false" fontClasses="fas fa-eye" [title]="t('total-read-time-tooltip', {count: stats.totalReadingTime | number})">
|
||||
{{stats.totalReadingTime | timeDuration}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="grid row g-0 pt-2 pb-2 d-flex justify-content-around">
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="releaseYears$" title="Release Years" label="series"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveUsers$" title="Most Active Users" label="reads"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveLibrary$" title="Popular Libraries" label="reads"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveSeries$" title="Popular Series" [image]="seriesImage" [handleClick]="openSeries">
|
||||
</app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="recentlyRead$" title="Recently Read" [image]="seriesImage" [handleClick]="openSeries"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="releaseYears$" [title]="t('release-years-title')" [label]="t('series')"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveUsers$" [title]="t('most-active-users-title')" [label]="t('reads')"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveLibrary$" [title]="t('popular-libraries-title')" [label]="t('reads')"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveSeries$" [title]="t('popular-series-title')" [image]="seriesImage" [handleClick]="openSeries">
|
||||
</app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="recentlyRead$" title="Recently Read" [image]="seriesImage" [handleClick]="openSeries"></app-stat-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="breakpoint$ | async as bp">
|
||||
<div class="row g-0 pt-2 pb-2">
|
||||
<app-top-readers></app-top-readers>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<div class="col-lg-6 col-md-12 mb-md-5">
|
||||
<app-file-breakdown-stats></app-file-breakdown-stats>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12">
|
||||
<app-publication-status-stats></app-publication-status-stats>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 pt-2 pb-2">
|
||||
<app-top-readers></app-top-readers>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||
<app-reading-activity [isAdmin]="true"></app-reading-activity>
|
||||
</div>
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<div class="col-lg-6 col-md-12 mb-md-5">
|
||||
<app-file-breakdown-stats></app-file-breakdown-stats>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||
<app-day-breakdown></app-day-breakdown>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12">
|
||||
<app-publication-status-stats></app-publication-status-stats>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||
<app-reading-activity [isAdmin]="true"></app-reading-activity>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2">
|
||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||
<app-day-breakdown></app-day-breakdown>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {ChangeDetectionStrategy, Component, DestroyRef, HostListener, inject, OnDestroy} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, DestroyRef, HostListener, inject} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {BehaviorSubject, map, Observable, ReplaySubject, shareReplay, Subject, takeUntil} from 'rxjs';
|
||||
import { map, Observable, ReplaySubject, shareReplay } from 'rxjs';
|
||||
import { FilterQueryParam } 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';
|
||||
|
|
@ -23,6 +23,7 @@ import { TopReadersComponent } from '../top-readers/top-readers.component';
|
|||
import { StatListComponent } from '../stat-list/stat-list.component';
|
||||
import { IconAndTitleComponent } from '../../../shared/icon-and-title/icon-and-title.component';
|
||||
import { NgIf, AsyncPipe, DecimalPipe } from '@angular/common';
|
||||
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-server-stats',
|
||||
|
|
@ -30,7 +31,7 @@ import { NgIf, AsyncPipe, DecimalPipe } from '@angular/common';
|
|||
styleUrls: ['./server-stats.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, IconAndTitleComponent, StatListComponent, TopReadersComponent, FileBreakdownStatsComponent, PublicationStatusStatsComponent, ReadingActivityComponent, DayBreakdownComponent, AsyncPipe, DecimalPipe, CompactNumberPipe, TimeDurationPipe, BytesPipe]
|
||||
imports: [NgIf, IconAndTitleComponent, StatListComponent, TopReadersComponent, FileBreakdownStatsComponent, PublicationStatusStatsComponent, ReadingActivityComponent, DayBreakdownComponent, AsyncPipe, DecimalPipe, CompactNumberPipe, TimeDurationPipe, BytesPipe, TranslocoModule]
|
||||
})
|
||||
export class ServerStatsComponent {
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ export class ServerStatsComponent {
|
|||
}
|
||||
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
|
||||
constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService,
|
||||
|
|
@ -108,7 +110,7 @@ export class ServerStatsComponent {
|
|||
this.metadataService.getAllGenres().subscribe(genres => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = genres.map(t => t.title);
|
||||
ref.componentInstance.title = 'Genres';
|
||||
ref.componentInstance.title = this.translocoService.translate('server-stats.genres');
|
||||
ref.componentInstance.clicked = (item: string) => {
|
||||
const params: any = {};
|
||||
params[FilterQueryParam.Genres] = genres.filter(g => g.title === item)[0].id;
|
||||
|
|
@ -122,7 +124,7 @@ export class ServerStatsComponent {
|
|||
this.metadataService.getAllTags().subscribe(tags => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = tags.map(t => t.title);
|
||||
ref.componentInstance.title = 'Tags';
|
||||
ref.componentInstance.title = this.translocoService.translate('server-stats.tags');
|
||||
ref.componentInstance.clicked = (item: string) => {
|
||||
const params: any = {};
|
||||
params[FilterQueryParam.Tags] = tags.filter(g => g.title === item)[0].id;
|
||||
|
|
@ -136,7 +138,7 @@ export class ServerStatsComponent {
|
|||
this.metadataService.getAllPeople().subscribe(people => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = [...new Set(people.map(person => person.name))];
|
||||
ref.componentInstance.title = 'People';
|
||||
ref.componentInstance.title = this.translocoService.translate('server-stats.people');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<ng-container *ngIf="data$ | async as data">
|
||||
<div class="card" style="width: 18rem;">
|
||||
<div class="card-header text-center">
|
||||
{{title}}
|
||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0" *ngIf="description && description.length > 0"></i>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item" [ngClass]="{'underline': handleClick !== undefined}" *ngFor="let item of data" (click)="doClick(item)">
|
||||
<ng-container *ngIf="image && image(item) as url">
|
||||
<app-image *ngIf="url && url.length > 0" width="32px" maxHeight="32px" class="img-top me-1" [imageUrl]="url"></app-image>
|
||||
</ng-container>
|
||||
{{item.name}} <span class="float-end" *ngIf="item.value >= 0">{{item.value | compactNumber}} {{label}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="card" style="width: 18rem;">
|
||||
<div class="card-header text-center">
|
||||
{{title}}
|
||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0" *ngIf="description && description.length > 0"></i>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item" [ngClass]="{'underline': handleClick !== undefined}" *ngFor="let item of data" (click)="doClick(item)">
|
||||
<ng-container *ngIf="image && image(item) as url">
|
||||
<app-image *ngIf="url && url.length > 0" width="32px" maxHeight="32px" class="img-top me-1" [imageUrl]="url"></app-image>
|
||||
</ng-container>
|
||||
{{item.name}} <span class="float-end" *ngIf="item.value >= 0">{{item.value | compactNumber}} {{label}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #tooltip></ng-template>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { CompactNumberPipe } from '../../../pipe/compact-number.pipe';
|
|||
import { ImageComponent } from '../../../shared/image/image.component';
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgIf, NgFor, NgClass, AsyncPipe } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-stat-list',
|
||||
|
|
@ -13,7 +14,7 @@ import { NgIf, NgFor, NgClass, AsyncPipe } from '@angular/common';
|
|||
styleUrls: ['./stat-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgbTooltip, NgFor, NgClass, ImageComponent, AsyncPipe, CompactNumberPipe]
|
||||
imports: [NgIf, NgbTooltip, NgFor, NgClass, ImageComponent, AsyncPipe, CompactNumberPipe, TranslocoModule]
|
||||
})
|
||||
export class StatListComponent {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +1,38 @@
|
|||
<div class="row g-0 mb-2 align-items-center">
|
||||
<ng-container *transloco="let t; read:'top-readers'">
|
||||
<div class="row g-0 mb-2 align-items-center">
|
||||
<div class="col-4">
|
||||
<h4>Top Readers</h4>
|
||||
<h4>{{t('title')}}</h4>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<form [formGroup]="formGroup" class="d-inline-flex float-end">
|
||||
<div class="d-flex">
|
||||
<label for="time-select-top-reads" class="form-check-label"></label>
|
||||
<select id="time-select-top-reads" class="form-select" formControlName="days"
|
||||
[class.is-invalid]="formGroup.get('days')?.invalid && formGroup.get('days')?.touched">
|
||||
<option *ngFor="let item of timePeriods" [value]="item.value">{{item.title}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-container>
|
||||
<div class="grid row g-0">
|
||||
<div class="card" *ngFor="let user of (users$ | async)">
|
||||
<div class="card-header text-center">
|
||||
{{user.username}}
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">Comics: {{user.comicsTime}} hrs</li>
|
||||
<li class="list-group-item">Manga: {{user.mangaTime}} hrs</li>
|
||||
<li class="list-group-item">Books: {{user.booksTime}} hrs</li>
|
||||
</ul>
|
||||
<form [formGroup]="formGroup" class="d-inline-flex float-end">
|
||||
<div class="d-flex">
|
||||
<label for="time-select-top-reads" class="form-check-label visually-hidden">{{t('time-selection-label')}}</label>
|
||||
<select id="time-select-top-reads" class="form-select" formControlName="days"
|
||||
[class.is-invalid]="formGroup.get('days')?.invalid && formGroup.get('days')?.touched">
|
||||
<option *ngFor="let item of timePeriods" [value]="item.value">{{t(item.title)}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-container>
|
||||
<div class="grid row g-0">
|
||||
<div class="card" *ngFor="let user of (users$ | async)">
|
||||
<div class="card-header text-center">
|
||||
{{user.username}}
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">{{t('comics-label', {value: user.comicsTime})}}</li>
|
||||
<li class="list-group-item">{{t('manga-label', {value: user.mangaTime})}}</li>
|
||||
<li class="list-group-item">{{t('books-label', {value: user.booksTime})}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,17 +4,23 @@ import {
|
|||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Observable, Subject, takeUntil, switchMap, shareReplay } from 'rxjs';
|
||||
import { Observable, switchMap, shareReplay } from 'rxjs';
|
||||
import { StatisticsService } from 'src/app/_services/statistics.service';
|
||||
import { TopUserRead } from '../../_models/top-reads';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { NgFor, AsyncPipe } from '@angular/common';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
export const TimePeriods: Array<{title: string, value: number}> = [{title: 'This Week', value: new Date().getDay() || 1}, {title: 'Last 7 Days', value: 7}, {title: 'Last 30 Days', value: 30}, {title: 'Last 90 Days', value: 90}, {title: 'Last Year', value: 365}, {title: 'All Time', value: 0}];
|
||||
export const TimePeriods: Array<{title: string, value: number}> =
|
||||
[{title: 'this-week', value: new Date().getDay() || 1},
|
||||
{title: 'last-7-days', value: 7},
|
||||
{title: 'last-30-days', value: 30},
|
||||
{title: 'last-90-days', value: 90},
|
||||
{title: 'last-year', value: 365},
|
||||
{title: 'all-time', value: 0}];
|
||||
|
||||
@Component({
|
||||
selector: 'app-top-readers',
|
||||
|
|
@ -22,7 +28,7 @@ export const TimePeriods: Array<{title: string, value: number}> = [{title: 'This
|
|||
styleUrls: ['./top-readers.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgFor, AsyncPipe]
|
||||
imports: [ReactiveFormsModule, NgFor, AsyncPipe, TranslocoModule]
|
||||
})
|
||||
export class TopReadersComponent implements OnInit {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,54 +1,60 @@
|
|||
<div class="row g-0 mt-4 mb-3 d-flex justify-content-around">
|
||||
<ng-container *transloco="let t; read:'user-stats-info-cards'">
|
||||
<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]="true" fontClasses="fa-regular fa-file-lines" title="Total Pages Read: {{totalPagesRead | number}}" (click)="openPageByYearList();$event.stopPropagation();">
|
||||
{{totalPagesRead | compactNumber}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-pages-read-label')" [clickable]="true" fontClasses="fa-regular fa-file-lines"
|
||||
[title]="t('total-pages-read-tooltip', {value: totalPagesRead | number})" (click)="openPageByYearList();$event.stopPropagation();">
|
||||
{{totalPagesRead | compactNumber}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Words Read" [clickable]="true" fontClasses="fa-regular fa-file-lines" title="Total Words Read: {{totalWordsRead | number}}" (click)="openWordByYearList();$event.stopPropagation();">
|
||||
{{totalWordsRead | compactNumber}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('total-words-read-label')" [clickable]="true" fontClasses="fa-regular fa-file-lines"
|
||||
[title]="t('total-words-read-tooltip', {value: totalWordsRead | number})" (click)="openWordByYearList();$event.stopPropagation();">
|
||||
{{totalWordsRead | compactNumber}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container >
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Time Spent Reading" [clickable]="false" fontClasses="fas fa-eye" title="Time Spent Reading: {{timeSpentReading | number}}">
|
||||
{{timeSpentReading | timeDuration}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('time-spent-reading-label')" [clickable]="false" fontClasses="fas fa-eye"
|
||||
[title]="t('time-spent-reading-tooltip', {value: timeSpentReading | number})">
|
||||
{{timeSpentReading | timeDuration}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Average Reading / Week" [clickable]="false" fontClasses="fas fa-eye">
|
||||
{{avgHoursPerWeekSpentReading | timeDuration}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('avg-reading-per-week-label')" [clickable]="false" fontClasses="fas fa-eye">
|
||||
{{avgHoursPerWeekSpentReading | timeDuration}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Chapters Read" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Chapters Read: {{chaptersRead | number}}">
|
||||
{{chaptersRead | compactNumber}} Chapters
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('chapters-read-label')" [clickable]="false" fontClasses="fa-regular fa-file-lines"
|
||||
[title]="t('chapters-read-tooltip', {value: (chaptersRead | number)})">
|
||||
{{t('chapters', {value: (chaptersRead | compactNumber)})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Last Active" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Last Active">
|
||||
{{lastActive | timeAgo}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('last-active-label')" [clickable]="false" fontClasses="fa-regular fa-calendar">
|
||||
{{lastActive | timeAgo}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { TimeDurationPipe } from '../../../pipe/time-duration.pipe';
|
|||
import { CompactNumberPipe as CompactNumberPipe_1 } from '../../../pipe/compact-number.pipe';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { IconAndTitleComponent } from '../../../shared/icon-and-title/icon-and-title.component';
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-stats-info-cards',
|
||||
|
|
@ -15,7 +16,7 @@ import { IconAndTitleComponent } from '../../../shared/icon-and-title/icon-and-t
|
|||
styleUrls: ['./user-stats-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [IconAndTitleComponent, DecimalPipe, CompactNumberPipe_1, TimeDurationPipe, TimeAgoPipe]
|
||||
imports: [IconAndTitleComponent, DecimalPipe, CompactNumberPipe_1, TimeDurationPipe, TimeAgoPipe, TranslocoModule]
|
||||
})
|
||||
export class UserStatsInfoCardsComponent {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
<div class="container-fluid" *ngIf="userId">
|
||||
<ng-container *transloco="let t; read:'user-stats'">
|
||||
<div class="container-fluid" *ngIf="userId">
|
||||
|
||||
<div class="row g-0 d-flex justify-content-around">
|
||||
<ng-container *ngIf="userStats$ | async as userStats">
|
||||
<app-user-stats-info-cards [totalPagesRead]="userStats.totalPagesRead" [totalWordsRead]="userStats.totalWordsRead" [timeSpentReading]="userStats.timeSpentReading"
|
||||
[chaptersRead]="userStats.chaptersRead" [lastActive]="userStats.lastActive" [avgHoursPerWeekSpentReading]="userStats.avgHoursPerWeekSpentReading"></app-user-stats-info-cards>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="userStats$ | async as userStats">
|
||||
<app-user-stats-info-cards [totalPagesRead]="userStats.totalPagesRead" [totalWordsRead]="userStats.totalWordsRead" [timeSpentReading]="userStats.timeSpentReading"
|
||||
[chaptersRead]="userStats.chaptersRead" [lastActive]="userStats.lastActive" [avgHoursPerWeekSpentReading]="userStats.avgHoursPerWeekSpentReading"></app-user-stats-info-cards>
|
||||
</ng-container>
|
||||
</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-reading-activity [userId]="userId" [isAdmin]="(isAdmin$ | async) || false" [individualUserMode]="true"></app-reading-activity>
|
||||
</div>
|
||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||
<app-reading-activity [userId]="userId" [isAdmin]="(isAdmin$ | async) || false" [individualUserMode]="true"></app-reading-activity>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2 " style="height: 242px">
|
||||
<app-stat-list [data$]="percentageRead$" label="% Read" title="Library Read Progress"></app-stat-list>
|
||||
<app-stat-list [data$]="percentageRead$" [label]="t('read-percentage')" [title]="t('library-read-progress-title')"></app-stat-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||
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";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-stats',
|
||||
|
|
@ -34,6 +35,7 @@ import { UserStatsInfoCardsComponent } from '../user-stats-info-cards/user-stats
|
|||
ReadingActivityComponent,
|
||||
StatListComponent,
|
||||
AsyncPipe,
|
||||
TranslocoModule,
|
||||
],
|
||||
})
|
||||
export class UserStatsComponent implements OnInit {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {inject, Pipe, PipeTransform} from '@angular/core';
|
||||
import { DayOfWeek } from 'src/app/_services/statistics.service';
|
||||
import {TranslocoService} from "@ngneat/transloco";
|
||||
|
||||
@Pipe({
|
||||
name: 'dayOfWeek',
|
||||
|
|
@ -7,15 +8,24 @@ import { DayOfWeek } from 'src/app/_services/statistics.service';
|
|||
})
|
||||
export class DayOfWeekPipe implements PipeTransform {
|
||||
|
||||
translocoService = inject(TranslocoService);
|
||||
|
||||
transform(value: DayOfWeek): string {
|
||||
switch(value) {
|
||||
case DayOfWeek.Monday: return 'Monday';
|
||||
case DayOfWeek.Tuesday: return 'Tuesday';
|
||||
case DayOfWeek.Wednesday: return 'Wednesday';
|
||||
case DayOfWeek.Thursday: return 'Thursday';
|
||||
case DayOfWeek.Friday: return 'Friday';
|
||||
case DayOfWeek.Saturday: return 'Saturday';
|
||||
case DayOfWeek.Sunday: return 'Sunday';
|
||||
case DayOfWeek.Monday:
|
||||
return this.translocoService.translate('day-of-week-pipe.monday');
|
||||
case DayOfWeek.Tuesday:
|
||||
return this.translocoService.translate('day-of-week-pipe.tuesday');
|
||||
case DayOfWeek.Wednesday:
|
||||
return this.translocoService.translate('day-of-week-pipe.wednesday');
|
||||
case DayOfWeek.Thursday:
|
||||
return this.translocoService.translate('day-of-week-pipe.thursday');
|
||||
case DayOfWeek.Friday:
|
||||
return this.translocoService.translate('day-of-week-pipe.friday');
|
||||
case DayOfWeek.Saturday:
|
||||
return this.translocoService.translate('day-of-week-pipe.saturday');
|
||||
case DayOfWeek.Sunday:
|
||||
return this.translocoService.translate('day-of-week-pipe.sunday');
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue