mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Stat Polish (#1775)
* SeriesGroup tag can now have comma separated value to allow a series to be a part of multiple collections. * Added a missing unit test * Refactored how collection tags are created to work in the scan loop reliably. * Added a unit test for RemoveTagsWithoutSeries * Fixed a bug in reading list title generation to avoid Volume 0 if the underlying file had a title set. Fixed a misconfigured unit test. * On User stats page, don't show the user selector on reading history, despite if youre an admin. Cleaned up how we show days with 0 reading events to be more clear. * Refactored the name of a component to reflect what it does * Removed plugin not using * Fix an issue where coalescing a key in epub might have multiple html files ending with the key. In this case, let's take the first. * Added PikaPods to the Readme * Tried to fix layout shift for charts, but need Robbie's help * Chart styling # Added: - Added: Added styling to force charts into their respective containers. # Removed: - Removed: Removed code blocking charts from being visible on mobile. * Merge conflict --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
c60fdcf6e0
commit
cad897015c
@ -731,7 +731,7 @@ public class BookService : IBookService
|
|||||||
if (mappings.ContainsKey(CleanContentKeys(key))) return key;
|
if (mappings.ContainsKey(CleanContentKeys(key))) return key;
|
||||||
|
|
||||||
// Fallback to searching for key (bad epub metadata)
|
// Fallback to searching for key (bad epub metadata)
|
||||||
var correctedKey = book.Content.Html.Keys.SingleOrDefault(s => s.EndsWith(key));
|
var correctedKey = book.Content.Html.Keys.FirstOrDefault(s => s.EndsWith(key));
|
||||||
if (!string.IsNullOrEmpty(correctedKey))
|
if (!string.IsNullOrEmpty(correctedKey))
|
||||||
{
|
{
|
||||||
key = correctedKey;
|
key = correctedKey;
|
||||||
|
@ -63,19 +63,6 @@ public class StatisticService : IStatisticService
|
|||||||
.Where(p => libraryIds.Contains(p.LibraryId))
|
.Where(p => libraryIds.Contains(p.LibraryId))
|
||||||
.SumAsync(p => p.PagesRead);
|
.SumAsync(p => p.PagesRead);
|
||||||
|
|
||||||
// var ids = await _context.AppUserProgresses
|
|
||||||
// .Where(p => p.AppUserId == userId)
|
|
||||||
// .Where(p => libraryIds.Contains(p.LibraryId))
|
|
||||||
// .Where(p => p.PagesRead > 0)
|
|
||||||
// .Select(p => new {p.ChapterId, p.SeriesId})
|
|
||||||
// .ToListAsync();
|
|
||||||
|
|
||||||
//var chapterIds = ids.Select(id => id.ChapterId);
|
|
||||||
|
|
||||||
// var timeSpentReading = await _context.Chapter
|
|
||||||
// .Where(c => chapterIds.Contains(c.Id))
|
|
||||||
// .SumAsync(c => c.AvgHoursToRead);
|
|
||||||
|
|
||||||
var timeSpentReading = await TimeSpentReadingForUsersAsync(new List<int>() {userId}, libraryIds);
|
var timeSpentReading = await TimeSpentReadingForUsersAsync(new List<int>() {userId}, libraryIds);
|
||||||
|
|
||||||
var totalWordsRead = (long) Math.Round(await _context.AppUserProgresses
|
var totalWordsRead = (long) Math.Round(await _context.AppUserProgresses
|
||||||
@ -364,12 +351,12 @@ public class StatisticService : IStatisticService
|
|||||||
if (days > 0)
|
if (days > 0)
|
||||||
{
|
{
|
||||||
var date = DateTime.Now.AddDays(days * -1);
|
var date = DateTime.Now.AddDays(days * -1);
|
||||||
query = query.Where(x => x.appUserProgresses.LastModified >= date && x.appUserProgresses.Created >= date);
|
query = query.Where(x => x.appUserProgresses.LastModified >= date);
|
||||||
}
|
}
|
||||||
|
|
||||||
var results = await query.GroupBy(x => new
|
var results = await query.GroupBy(x => new
|
||||||
{
|
{
|
||||||
Day = x.appUserProgresses.Created.Date,
|
Day = x.appUserProgresses.LastModified.Date,
|
||||||
x.series.Format
|
x.series.Format
|
||||||
})
|
})
|
||||||
.Select(g => new PagesReadOnADayCount<DateTime>
|
.Select(g => new PagesReadOnADayCount<DateTime>
|
||||||
@ -389,7 +376,25 @@ public class StatisticService : IStatisticService
|
|||||||
if (results.Any(d => d.Value == date)) continue;
|
if (results.Any(d => d.Value == date)) continue;
|
||||||
results.Add(new PagesReadOnADayCount<DateTime>()
|
results.Add(new PagesReadOnADayCount<DateTime>()
|
||||||
{
|
{
|
||||||
Format = MangaFormat.Unknown,
|
Format = MangaFormat.Archive,
|
||||||
|
Value = date,
|
||||||
|
Count = 0
|
||||||
|
});
|
||||||
|
results.Add(new PagesReadOnADayCount<DateTime>()
|
||||||
|
{
|
||||||
|
Format = MangaFormat.Epub,
|
||||||
|
Value = date,
|
||||||
|
Count = 0
|
||||||
|
});
|
||||||
|
results.Add(new PagesReadOnADayCount<DateTime>()
|
||||||
|
{
|
||||||
|
Format = MangaFormat.Pdf,
|
||||||
|
Value = date,
|
||||||
|
Count = 0
|
||||||
|
});
|
||||||
|
results.Add(new PagesReadOnADayCount<DateTime>()
|
||||||
|
{
|
||||||
|
Format = MangaFormat.Image,
|
||||||
Value = date,
|
Value = date,
|
||||||
Count = 0
|
Count = 0
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
@ -97,6 +97,10 @@ We would like to extend a big thank you to [<img src="/Logo/hosting-sponsor.png"
|
|||||||
We would like to extend a big thank you to [Huntr](https://huntr.dev/repos/kareadita/kavita) who has worked with Kavita in reporting security vulnerabilities. If you are interested in
|
We would like to extend a big thank you to [Huntr](https://huntr.dev/repos/kareadita/kavita) who has worked with Kavita in reporting security vulnerabilities. If you are interested in
|
||||||
being paid to help secure Kavita, please give them a try.
|
being paid to help secure Kavita, please give them a try.
|
||||||
|
|
||||||
|
## PikaPods
|
||||||
|
If you are looking to try your hand at self-hosting but lack the machine, [PikaPods](https://www.pikapods.com/pods?run=kavita) is a great service that
|
||||||
|
allows you to easily spin up a server. 20% of app revenues are contributed back to Kavita via OpenCollective.
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
padding: 0 5px 0;
|
padding: 0 5px 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: calc(100vh - 56px);
|
||||||
|
|
||||||
&.closed {
|
&.closed {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
<div class="row g-0 mb-2">
|
<div class="dashboard-card-content">
|
||||||
<h4>Day Breakdown</h4>
|
<div class="row g-0 mb-2">
|
||||||
</div>
|
<h4>Day Breakdown</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ngx-charts-bar-vertical
|
<ngx-charts-bar-vertical
|
||||||
class="dark"
|
class="dark"
|
||||||
[view]="view"
|
[results]="dayBreakdown$ | async"
|
||||||
[results]="dayBreakdown$ | async"
|
[xAxis]="true"
|
||||||
[xAxis]="true"
|
[yAxis]="true"
|
||||||
[yAxis]="true"
|
[legend]="showLegend"
|
||||||
[legend]="showLegend"
|
[showXAxisLabel]="true"
|
||||||
[showXAxisLabel]="true"
|
[showYAxisLabel]="true"
|
||||||
[showYAxisLabel]="true"
|
xAxisLabel="Day of Week"
|
||||||
xAxisLabel="Day of Week"
|
yAxisLabel="Reading Events">
|
||||||
yAxisLabel="Reading Events">
|
</ngx-charts-bar-vertical>
|
||||||
</ngx-charts-bar-vertical>
|
</div>
|
@ -1,3 +1,11 @@
|
|||||||
::ng-deep .dark .ngx-charts text {
|
::ng-deep .dark .ngx-charts text {
|
||||||
fill: #a0aabe;
|
fill: #a0aabe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-card-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 242px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
@ -16,7 +16,7 @@ export class DayBreakdownComponent implements OnDestroy {
|
|||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
view: [number, number] = [700, 400];
|
view: [number, number] = [0,0];
|
||||||
gradient: boolean = true;
|
gradient: boolean = true;
|
||||||
showLegend: boolean = true;
|
showLegend: boolean = true;
|
||||||
showLabels: boolean = true;
|
showLabels: boolean = true;
|
||||||
|
@ -1,71 +1,74 @@
|
|||||||
<div class="row g-0 mb-2">
|
<div class="dashboard-card-content">
|
||||||
<div class="col-8">
|
<div class="row g-0 mb-2">
|
||||||
<h4><span>Format</span>
|
<div class="col-8">
|
||||||
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0"></i>
|
<h4><span>Format</span>
|
||||||
</h4>
|
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0"></i>
|
||||||
</div>
|
</h4>
|
||||||
<div class="col-4">
|
</div>
|
||||||
<form>
|
<div class="col-4">
|
||||||
<div class="form-check form-switch mt-2">
|
<form>
|
||||||
<input id="pub-file-breakdown-viz" type="checkbox" class="form-check-input" [formControl]="formControl" role="switch">
|
<div class="form-check form-switch mt-2">
|
||||||
<label for="pub-file-breakdown-viz" class="form-check-label">{{formControl.value ? 'Vizualization' : 'Data Table' }}</label>
|
<input id="pub-file-breakdown-viz" type="checkbox" class="form-check-input" [formControl]="formControl" role="switch">
|
||||||
</div>
|
<label for="pub-file-breakdown-viz" class="form-check-label">{{formControl.value ? 'Vizualization' : 'Data Table' }}</label>
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</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-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-light 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>
|
||||||
|
|
||||||
|
|
||||||
</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-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-light 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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,3 +2,11 @@
|
|||||||
top: unset !important;
|
top: unset !important;
|
||||||
transform: unset !important;
|
transform: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-card-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 242px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
@ -1,51 +1,54 @@
|
|||||||
<div class="row g-0 mb-2">
|
<div class="dashboard-card-content">
|
||||||
<div class="col-8">
|
|
||||||
<h4><span>Publication Status</span>
|
<div class="row g-0 mb-2">
|
||||||
</h4>
|
<div class="col-8">
|
||||||
</div>
|
<h4><span>Publication Status</span>
|
||||||
<div class="col-4">
|
</h4>
|
||||||
<form>
|
</div>
|
||||||
<div class="form-check form-switch mt-2">
|
<div class="col-4">
|
||||||
<input id="pub-status-viz" type="checkbox" class="form-check-input" [formControl]="formControl" role="switch">
|
<form>
|
||||||
<label for="pub-status-viz" class="form-check-label">{{formControl.value ? 'Vizualization' : 'Data Table' }}</label>
|
<div class="form-check form-switch mt-2">
|
||||||
</div>
|
<input id="pub-status-viz" type="checkbox" class="form-check-input" [formControl]="formControl" role="switch">
|
||||||
</form>
|
<label for="pub-status-viz" class="form-check-label">{{formControl.value ? 'Vizualization' : 'Data Table' }}</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</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-light 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>
|
||||||
</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-light 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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,11 @@
|
|||||||
::ng-deep .pie-label {
|
::ng-deep .pie-label {
|
||||||
color: var(--body-text-color) !important;
|
color: var(--body-text-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-card-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 242px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
@ -1,52 +0,0 @@
|
|||||||
<ng-container>
|
|
||||||
<div class="row g-0 mb-2 align-items-center">
|
|
||||||
<div class="col-4">
|
|
||||||
<h4>Top Readers</h4>
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<form [formGroup]="formGroup" class="d-inline-flex float-end">
|
|
||||||
<div class="d-flex me-1" *ngIf="isAdmin">
|
|
||||||
<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>
|
|
||||||
<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="Reading Activity"
|
|
||||||
[timeline]="false"
|
|
||||||
[results]="data"
|
|
||||||
>
|
|
||||||
</ngx-charts-line-chart>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
<ng-template #noData>
|
|
||||||
No Reading progress
|
|
||||||
</ng-template>
|
|
||||||
</ng-container>
|
|
@ -0,0 +1,54 @@
|
|||||||
|
<ng-container>
|
||||||
|
<div class="dashboard-card-content">
|
||||||
|
<div class="row g-0 mb-2 align-items-center">
|
||||||
|
<div class="col-4">
|
||||||
|
<h4>Top Readers</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>
|
||||||
|
<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="Reading Activity"
|
||||||
|
[timeline]="false"
|
||||||
|
[results]="data"
|
||||||
|
>
|
||||||
|
</ngx-charts-line-chart>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<ng-template #noData>
|
||||||
|
No Reading progress
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
@ -12,17 +12,18 @@ const options: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" };
|
|||||||
const mangaFormatPipe = new MangaFormatPipe();
|
const mangaFormatPipe = new MangaFormatPipe();
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-read-by-day-and',
|
selector: 'app-reading-activity',
|
||||||
templateUrl: './read-by-day-and.component.html',
|
templateUrl: './reading-activity.component.html',
|
||||||
styleUrls: ['./read-by-day-and.component.scss'],
|
styleUrls: ['./reading-activity.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class ReadByDayAndComponent implements OnInit, OnDestroy {
|
export class ReadingActivityComponent implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Only show for one user
|
* Only show for one user
|
||||||
*/
|
*/
|
||||||
@Input() userId: number = 0;
|
@Input() userId: number = 0;
|
||||||
@Input() isAdmin: boolean = true;
|
@Input() isAdmin: boolean = true;
|
||||||
|
@Input() individualUserMode: boolean = false;
|
||||||
|
|
||||||
view: [number, number] = [0, 400];
|
view: [number, number] = [0, 400];
|
||||||
formGroup: FormGroup = new FormGroup({
|
formGroup: FormGroup = new FormGroup({
|
@ -92,26 +92,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="breakpoint$ | async as bp">
|
<ng-container *ngIf="breakpoint$ | async as bp">
|
||||||
<div class="row g-0 pt-2 pb-2" *ngIf="bp > Breakpoint.Mobile">
|
<div class="row g-0 pt-2 pb-2">
|
||||||
<app-top-readers></app-top-readers>
|
<app-top-readers></app-top-readers>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0 pt-4 pb-2" style="height: 242px" *ngIf="bp > Breakpoint.Mobile">
|
<div class="row g-0 pt-4 pb-2">
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="col-lg-6 col-md-12 mb-md-5">
|
||||||
<app-file-breakdown-stats></app-file-breakdown-stats>
|
<app-file-breakdown-stats></app-file-breakdown-stats>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="col-lg-6 col-md-12">
|
||||||
<app-publication-status-stats></app-publication-status-stats>
|
<app-publication-status-stats></app-publication-status-stats>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0 pt-4 pb-2 " style="height: 242px" *ngIf="bp > Breakpoint.Mobile">
|
<div class="row g-0 pt-4 pb-2">
|
||||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||||
<app-read-by-day-and [isAdmin]="true"></app-read-by-day-and>
|
<app-reading-activity [isAdmin]="true"></app-reading-activity>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0 pt-4 pb-2 " style="height: 242px" *ngIf="bp > Breakpoint.Mobile">
|
<div class="row g-0 pt-4 pb-2">
|
||||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||||
<app-day-breakdown></app-day-breakdown>
|
<app-day-breakdown></app-day-breakdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,3 +8,4 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<div class="row g-0 pt-4 pb-2 " style="height: 242px">
|
<div class="row g-0 pt-4 pb-2 " style="height: 242px">
|
||||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||||
<app-read-by-day-and [userId]="userId" [isAdmin]="(isAdmin$ | async) || false"></app-read-by-day-and>
|
<app-reading-activity [userId]="userId" [isAdmin]="(isAdmin$ | async) || false" [individualUserMode]="true"></app-reading-activity>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import { MangaFormatStatsComponent } from './_components/manga-format-stats/mang
|
|||||||
import { FileBreakdownStatsComponent } from './_components/file-breakdown-stats/file-breakdown-stats.component';
|
import { FileBreakdownStatsComponent } from './_components/file-breakdown-stats/file-breakdown-stats.component';
|
||||||
import { PipeModule } from '../pipe/pipe.module';
|
import { PipeModule } from '../pipe/pipe.module';
|
||||||
import { TopReadersComponent } from './_components/top-readers/top-readers.component';
|
import { TopReadersComponent } from './_components/top-readers/top-readers.component';
|
||||||
import { ReadByDayAndComponent } from './_components/read-by-day-and/read-by-day-and.component';
|
import { ReadingActivityComponent } from './_components/reading-activity/reading-activity.component';
|
||||||
import { GenericListModalComponent } from './_components/_modals/generic-list-modal/generic-list-modal.component';
|
import { GenericListModalComponent } from './_components/_modals/generic-list-modal/generic-list-modal.component';
|
||||||
import { DayBreakdownComponent } from './_components/day-breakdown/day-breakdown.component';
|
import { DayBreakdownComponent } from './_components/day-breakdown/day-breakdown.component';
|
||||||
import { DayOfWeekPipe } from './_pipes/day-of-week.pipe';
|
import { DayOfWeekPipe } from './_pipes/day-of-week.pipe';
|
||||||
@ -31,7 +31,7 @@ import { DayOfWeekPipe } from './_pipes/day-of-week.pipe';
|
|||||||
MangaFormatStatsComponent,
|
MangaFormatStatsComponent,
|
||||||
FileBreakdownStatsComponent,
|
FileBreakdownStatsComponent,
|
||||||
TopReadersComponent,
|
TopReadersComponent,
|
||||||
ReadByDayAndComponent,
|
ReadingActivityComponent,
|
||||||
GenericListModalComponent,
|
GenericListModalComponent,
|
||||||
DayBreakdownComponent,
|
DayBreakdownComponent,
|
||||||
DayOfWeekPipe
|
DayOfWeekPipe
|
||||||
|
Loading…
x
Reference in New Issue
Block a user