From 4548dcb1ebd72073ea917dc31809499cc8eed992 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Sun, 11 Dec 2022 16:29:46 -0600 Subject: [PATCH] Misc UI Tweaks (#1692) * Added a timeAgo pipe which shows live updates for a few areas. * Fixed some wording on stats page. Changed Total People count to just work on distinct names and not count multiple for different roles. * Tweaked the compact number so it only shows one decimal * Fixed a bug --- API/Services/StatisticService.cs | 10 +- .../manage-library.component.html | 2 +- .../series-info-cards.component.html | 2 +- UI/Web/src/app/pipe/compact-number.pipe.ts | 14 ++ UI/Web/src/app/pipe/pipe.module.ts | 5 +- UI/Web/src/app/pipe/time-ago.pipe.ts | 123 ++++++++++++++++++ .../file-breakdown-stats.component.html | 2 +- .../user-stats-info-cards.component.html | 2 +- openapi.json | 2 +- 9 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 UI/Web/src/app/pipe/time-ago.pipe.ts diff --git a/API/Services/StatisticService.cs b/API/Services/StatisticService.cs index ea66c9a98..7a2153bb2 100644 --- a/API/Services/StatisticService.cs +++ b/API/Services/StatisticService.cs @@ -240,13 +240,21 @@ public class StatisticService : IStatisticService .ProjectTo(_mapper.ConfigurationProvider) .AsEnumerable(); + var distinctPeople = _context.Person + .AsSplitQuery() + .AsEnumerable() + .GroupBy(sm => sm.NormalizedName) + .Select(sm => sm.Key) + .Distinct() + .Count(); + return new ServerStatistics() { ChapterCount = await _context.Chapter.CountAsync(), SeriesCount = await _context.Series.CountAsync(), TotalFiles = await _context.MangaFile.CountAsync(), TotalGenres = await _context.Genre.CountAsync(), - TotalPeople = await _context.Person.CountAsync(), + TotalPeople = distinctPeople, TotalSize = await _context.MangaFile.SumAsync(m => m.Bytes), TotalTags = await _context.Tag.CountAsync(), VolumeCount = await _context.Volume.Where(v => v.Number != 0).CountAsync(), diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.html b/UI/Web/src/app/admin/manage-library/manage-library.component.html index 873ef4141..3f34345d9 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.html +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.html @@ -21,7 +21,7 @@ Last Scanned: Never - {{library.lastScanned | date: 'short'}} + {{library.lastScanned | timeAgo}} diff --git a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html index 9675711b5..09420410e 100644 --- a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html +++ b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.html @@ -52,7 +52,7 @@
- {{series.latestReadDate | date:'shortDate'}} + {{series.latestReadDate | timeAgo}}
diff --git a/UI/Web/src/app/pipe/compact-number.pipe.ts b/UI/Web/src/app/pipe/compact-number.pipe.ts index 78ff2ef82..e8d3b92f1 100644 --- a/UI/Web/src/app/pipe/compact-number.pipe.ts +++ b/UI/Web/src/app/pipe/compact-number.pipe.ts @@ -7,12 +7,26 @@ const formatter = new Intl.NumberFormat('en-GB', { maximumSignificantDigits: 3 }); +const formatterForDoublePercision = new Intl.NumberFormat('en-GB', { + //@ts-ignore + notation: 'compact', // https://github.com/microsoft/TypeScript/issues/36533 + maximumSignificantDigits: 2 +}); + +const specialCases = [4, 7, 10, 13]; + @Pipe({ name: 'compactNumber' }) export class CompactNumberPipe implements PipeTransform { transform(value: number): string { + + if (value < 1000) return value + ''; + if (specialCases.includes((value + '').length)) { // from 4, every 3 will have a case where we need to override + return formatterForDoublePercision.format(value); + } + return formatter.format(value); } diff --git a/UI/Web/src/app/pipe/pipe.module.ts b/UI/Web/src/app/pipe/pipe.module.ts index 527c23172..e98720776 100644 --- a/UI/Web/src/app/pipe/pipe.module.ts +++ b/UI/Web/src/app/pipe/pipe.module.ts @@ -16,6 +16,7 @@ import { LibraryTypePipe } from './library-type.pipe'; import { SafeStylePipe } from './safe-style.pipe'; import { DefaultDatePipe } from './default-date.pipe'; import { BytesPipe } from './bytes.pipe'; +import { TimeAgoPipe } from './time-ago.pipe'; @@ -37,6 +38,7 @@ import { BytesPipe } from './bytes.pipe'; SafeStylePipe, DefaultDatePipe, BytesPipe, + TimeAgoPipe, ], imports: [ CommonModule, @@ -57,7 +59,8 @@ import { BytesPipe } from './bytes.pipe'; LibraryTypePipe, SafeStylePipe, DefaultDatePipe, - BytesPipe + BytesPipe, + TimeAgoPipe ] }) export class PipeModule { } diff --git a/UI/Web/src/app/pipe/time-ago.pipe.ts b/UI/Web/src/app/pipe/time-ago.pipe.ts new file mode 100644 index 000000000..da6546df0 --- /dev/null +++ b/UI/Web/src/app/pipe/time-ago.pipe.ts @@ -0,0 +1,123 @@ +import { ChangeDetectorRef, NgZone, OnDestroy, Pipe, PipeTransform } from '@angular/core'; + +/** + * MIT License + +Copyright (c) 2016 Andrew Poyntz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +This code was taken from https://github.com/AndrewPoyntz/time-ago-pipe/blob/master/time-ago.pipe.ts +and modified + */ + +@Pipe({ + name: 'timeAgo', + pure: false +}) +export class TimeAgoPipe implements PipeTransform, OnDestroy { + + private timer: number | null = null; + constructor(private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone) {} + transform(value:string) { + this.removeTimer(); + const d = new Date(value); + const now = new Date(); + const seconds = Math.round(Math.abs((now.getTime() - d.getTime()) / 1000)); + const timeToUpdate = (Number.isNaN(seconds)) ? 1000 : this.getSecondsUntilUpdate(seconds) * 1000; + + this.timer = this.ngZone.runOutsideAngular(() => { + if (typeof window !== 'undefined') { + return window.setTimeout(() => { + this.ngZone.run(() => this.changeDetectorRef.markForCheck()); + }, timeToUpdate); + } + return null; + }); + + const minutes = Math.round(Math.abs(seconds / 60)); + const hours = Math.round(Math.abs(minutes / 60)); + const days = Math.round(Math.abs(hours / 24)); + const months = Math.round(Math.abs(days/30.416)); + const years = Math.round(Math.abs(days/365)); + + if (Number.isNaN(seconds)){ + return ''; + } + + if (seconds <= 45) { + return 'just now'; + } + if (seconds <= 90) { + return 'a minute ago'; + } + if (minutes <= 45) { + return minutes + ' minutes ago'; + } + if (minutes <= 90) { + return 'an hour ago'; + } + if (hours <= 22) { + return hours + ' hours ago'; + } + if (hours <= 36) { + return 'a day ago'; + } + if (days <= 25) { + return days + ' days ago'; + } + if (days <= 45) { + return 'a month ago'; + } + if (days <= 345) { + return months + ' months ago'; + } + if (days <= 545) { + return 'a year ago'; + } + return years + ' years ago'; + } + + ngOnDestroy(): void { + this.removeTimer(); + } + + private removeTimer() { + if (this.timer) { + window.clearTimeout(this.timer); + this.timer = null; + } + } + + private getSecondsUntilUpdate(seconds:number) { + const min = 60; + const hr = min * 60; + const day = hr * 24; + if (seconds < min) { // less than 1 min, update every 2 secs + return 2; + } else if (seconds < hr) { // less than an hour, update every 30 secs + return 30; + } else if (seconds < day) { // less then a day, update every 5 mins + return 300; + } else { // update every hour + return 3600; + } + } + +} diff --git a/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.html b/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.html index 5206093e1..355da31d2 100644 --- a/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.html +++ b/UI/Web/src/app/statistics/_components/file-breakdown-stats/file-breakdown-stats.component.html @@ -14,7 +14,7 @@ -Non 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. +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. diff --git a/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.html b/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.html index bc41f1e8e..2dc0c5750 100644 --- a/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.html +++ b/UI/Web/src/app/statistics/_components/user-stats-info-cards/user-stats-info-cards.component.html @@ -29,7 +29,7 @@
- {{lastActive | date:'short'}} + {{lastActive | timeAgo}}
diff --git a/openapi.json b/openapi.json index 28c08f2e7..047c374fe 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.6.1.11" + "version": "0.6.1.12" }, "servers": [ {